You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Claimed on openxFactory#656 in 5901575394. Authored ahead as a DRAFT, which did not go READY before T063 landed and the holder said so. T063 has landed (openxFactory#1218 → a883bbf6, 2026-10-02 23:37:43Z), which closes phase 2, so phase 3 is open. #60 (7ff434d9) and #67 (66ff7257) ahead of it have landed.
What it does, by R1Q16's four parts
(i) The document server starts it as its own child, and reports it.
opendox generate-and-open --local (or OPENDOX_INSTALL_MODE=local) starts postgres as a direct child of the process serving the document surface. It uses subprocess.Popen and never pg_ctl, which would re-parent it.
It prints the socket, the pid and what it migrated.
runtime status reports database_bundle (data_dir, socket_dir, pid). It reads the pid from the server's own postmaster.pid and believes it only while a postgres runs there.
(ii) Started and migrated, and nothing more.
initdb runs once per data directory.
An idempotent bootstrap makes the database and the SERVED role. Its grants are the compose stack's (init-runtime-role.sh): CONNECT, USAGE on public, and DML on what the owner creates, by default privileges.
Then migrations.MigrationRunner runs as the owner, with the served role and database declared. The run narrows the ledger to SELECT for the served role and verifies its access, exactly as a hosted runtime migrate does.
runtime migrate under local migrates the bundle too.
(iii) It ships as the opendox[local] extra, which is opendox[runtime] plus pixeltable-pgserver>=0.6.0 (RULED, openxFactory#656 5916000030 item 2). The test extra joins it, so F9.1's .[test] install still runs every case.
(iv) It stops with the entry point.
SIGTERM is read as the Ctrl-C the serve loop already stops on, followed by a PostgreSQL fast shutdown (then an immediate one, then SIGKILL, each bounded).
The backstop is PR_SET_PDEATHSIG on Linux, so a SIGKILLed entry point still takes its server with it.
The server runs in its own session, so a terminal's Ctrl-C reaches the entry point, and the stop happens in order.
13.1's fixed identity
The data and socket directories live under OPENDOX_STATE_DIR, at <state>/postgres/data and <state>/postgres/run.
The setting is new. It defaults to $XDG_STATE_HOME/opendox, else ~/.local/state/opendox, and must be absolute, because the server's process and a runtime status run from elsewhere must derive the same socket.
A state directory too long for the kernel's sun_path is refused, naming the setting.
No TCP listener:listen_addresses is empty, and the socket directory is narrowed to 0700.
initdb runs with --auth-local=peer --auth-host=reject.
Before every launch the bundle rewrites pg_hba.conf and pg_ident.conf (atomically, mode 0600). pg_hba.conf holds one rule, local all all peer map=opendox, plus host … reject for IPv4 and IPv6. pg_ident.conf maps the running OS user, and nobody else, to opendox and opendox_runtime.
The kernel reports the connecting uid, so the DSNs carry no password because there is none. A cluster an older build left as trust is put back to peer on its next start.
Both DSNs are supplied: two users (owner opendox for migrations, opendox_runtime for serving) over the one socket, with port spelled so a stray PGPORT cannot redirect libpq. They pass T071's three checks for the reason those exist: one dialect, one database, and never one credential in both settings.
An operator DSN given beside local is refused by name. It is added to T070's HOSTED_ONLY_SETTINGS, a holder reading on openxFactory#656 that Brett may overrule.
A second entry point on the same state directory is refused, naming the running pid. One install's database belongs to one entry point at a time.
The migrations gap (assigned to T072 by the holder)
The migrations were not package data. migrations/ sits at the repository root, and only the image copies it (WORKDIR /app), so pip install "opendox[local]" run outside a checkout had nothing to apply.
Now pyproject.toml's [tool.setuptools.data-files] maps migrations/*.sql into the wheel's data directory (share/opendox/migrations). The root migrations/ does not move.
config.migrations_dir resolves an unset OPENDOX_MIGRATIONS_DIR in this order:
migrations wherever the working directory has one (a checkout, or the image's /app), which is today's default, unchanged;
otherwise the copy the installed distribution's RECORD lists (packaged_migrations_dir);
for an editable install, which installs no data files, the source tree's own migrations/.
The canonical digest gate is what proves any copy found is the pinned one.
builds this package's wheel offline (--no-build-isolation, --no-index);
installs it under a --prefix outside the checkout;
runs from a directory with nomigrations/, asserting that opendox is the wheel's copy and that the migrations dir is under the prefix's share/opendox;
migrates the bundled server there (applied == ["0001", "0002"]).
The server package (RULED, openxFactory#656 5916000030 item 2: "pixeltable-pgserver (Recommended)")
pixeltable-pgserver 0.6.0, the maintained fork of pgserver, uploaded 2026-07-14. Apache-2.0, as its dist-info LICENSE and OSI classifier say. It carries PostgreSQL 16.14 under the PostgreSQL License (initdb --version and postgres --version from pixeltable_pgserver/pginstall/bin). It also carries an 18.4 under pginstall18/, which this package does not use.
Only its binaries are used, found with importlib.util.find_spec("pixeltable_pgserver") without importing it. Its own manager is not used, because:
it daemonizes through pg_ctl, against (i);
it stops from atexit, which SIGTERM never runs, against (iv);
it may put the socket under the user's runtime directory, opened to 0777, against 13.1.
Linkage, re-verified on the installed wheel (readelf -d, ldd):
initdb needs those less libz and libdl, plus the wheel's own vendored libpq. That libpq resolves through RPATH $ORIGIN/../../../pixeltable_pgserver.libs and itself needs only libc, libm and libpthread;
the server's loadable modules need libc, and one needs the vendored libpq.
So the system libraries are the C library and libz only: no system PostgreSQL, no ICU.
Its floor and its size:
Wheels exist for cp310 to cp314, on Linux x86_64 and aarch64, macOS and Windows.
The Linux wheels are tagged manylinux_2_27 and manylinux_2_28, so they need glibc 2.27 or later. That is the tag's floor. The highest GLIBC symbol any binary or module needs is 2.25, by objdump -T.
Each wheel is about 24.7 MB (the cp312 x86_64 wheel is 24,704,230 bytes), because it carries two server majors.
It pulls in fasteners, platformdirs, psutil and typing-extensions, which nothing here imports. All four were already pinned.
Superseded:pgserver 0.1.4 carried PostgreSQL 16.2 and had no wheel after cp312 (Copilot r4139811507 and r4139811528, both now resolved with this ruling cited). Also rejected: postgresql-binaries, which links the system's ICU and untars at first use, and pgembed, which is PostgreSQL 17.
Outside src/ and tests/
pyproject.toml: the local extra, the test extra joining it, setuptools>=70.1 in test (the wheel test's offline build; 70.1 is the first release that builds a wheel with no wheel package), and the data-files map. It is a single-writer file (T057 → T072). T057, openDox's own validator and its input set (7.1, 7.1a, 7.1b, 7.2) (plan 034) #58 (T057) adds package data there, and the merge-from-main round takes it.
constraints-cpython312-linux.txt, in its own commit as its header asks. It was extended under its own pins in a clean 3.12.3 venv (psutil==7.2.2, platformdirs==4.12.2, fasteners==0.20 and setuptools==84.0.0 new). At 84a6c041 it was re-resolved in a clean environment under the pins less pgserver. The one line that moved is pgserver==0.1.4 → pixeltable-pgserver==0.6.0.
deploy/ — one line, and the task requires it.deploy/compose/.env.example gains OPENDOX_STATE_DIR=, because test_every_runtime_setting_is_documented_in_env_example requires every SETTINGS entry there. The compose stack is hosted and never reads it. docs/: untouched.
Not touched:serve.py (T073 adds the install block) and validate.yml. It already installs .[runtime,test], and test now carries local.
Existing tests changed:
tests/test_doxbench_entrypoint.py stands the bundle in, with a tripwire. Its cases test the model port, which reads nothing from the store (R1Q16 (ii)).
T070's test_install_mode.py and test_install_mode_entrypoint.py stop passing DSNs beside local.
test_runtime_surface.py declares opendox.runtime.bundle stdlib-only at import, because opendox.cli imports it and opendox --help runs with no extra installed.
The falsifier
F13.1's runtime status block and its TCP-listener block, verbatim in their assertions (f13-1-local.sh), against a server generate-and-open --local started in the background, with no broker and no operator database, under set -euo pipefail.
Since the merge round (19e32f0c), the start is the REAL entry point. It is python -m opendox.cli generate-and-open --local, with the validator on, over T050's tests/fixtures/plain-documents copied into a fresh repository, as F13.1's preamble does. The stand-in driver is deleted. One deviation remains, and it does not weaken a check:
Generation is stood in. Retired at 19e32f0c. Until then the start went through tests_runtime/local_entrypoint_driver.py, because this stack's base predated T055/T056's standalone generate.
The pid comes from runtime status. The TCP-listener block reads the server's pid from runtime status's database_bundle, not from caps.json. /capabilities' install block is T073's, so F13.1's caps.json block is T073's to run.
The corpus is a one-file stand-in. Retired at 19e32f0c: it is T050's fixture now.
BEFORE is #67's head b50e3b1; AFTER is this branch:
=== BEFORE (b50e3b1)
ready=1
runtime status rc=1
AssertionError: no bundled database answered: None OPENDOX_DATABASE_URL is required and is not set: …
FAIL status-block
AssertionError: the bundle reports no server pid: None
FAIL tcp-listener-block
=== AFTER (this branch, set -euo pipefail, exit 0)
ready=1
runtime status rc=0
PASS status-block
server pid 638810, sockets ['3692911'], TCP LISTEN rows: none
PASS tcp-listener-block
PASS stops-with-entry-point (pid 638810 gone)
--- server stdout:
database /tmp/tmp.v0wydKm5Zm/postgres/run (bundled, pid 638810, migrations applied now: ['0001', '0002'])
T070's F13.1 refusal probes and F13.1's load_settings block still pass on this branch (F13.1 REFUSALS + T070 PAIR: ALL PASSED).
In the suite, tests_runtime/test_bundled_postgres.py runs the same two blocks on the same background launch, and adds three checks: the server's PPid is the entry point's pid (i), the socket directory is 0700, and SIGTERM ends the entry point with exit 0 and the server gone (iv). Beside that it has a SIGKILL case (the parent-death backstop), the second-server refusal, runtime migrate under local, the wheel case above, and the layout and refusal cases. Under CI it fails rather than skips if the server is missing, because validate.yml pins EXPECT_SKIPPED=11 exactly.
A mutant of each new refusal and guarantee, killed
Each mutant was applied alone, and test_bundled_postgres.py plus test_install_mode.py were run with -x, with a 240 s bound so a hang could not pass for a kill:
A defect this PR's own test found in itself. The first cut of the wheel case ran pip install --prefix without --ignore-installed. pip then read the suite's own editable opendox as the installed copy of the same project and uninstalled it, emptying the environment the suite runs in (measured: pip list lost opendox and both console scripts). The flag is now there, with a comment, and the case asserts afterwards that the suite's own opendox still resolves.
The repo's own suite
Full python -m pytest -q, LANG=C.UTF-8, CI=true, against a postgres:16 like validate.yml's:
84a6c041 (the carrier and peer authentication, as ruled)
2613
2602
11
0
0
f8e6e9e9 (fix round 10)
2619
2608
11
0
0
fe232fe (merges #67's c8fac05e, carrying main 047bb4fa), before its edits
—
—
—
3 (the bundled background cases)
—
19e32f0c (the merge round's edits)
3195
3184
11
0
0
6eb0bbdb (merges #67's cdf7382b; the env probe expects the child's own state dir)
3204
3193
11
0
0
fedfa75d (fix round 11, 0488f5bd, then merges #67's d1de1fd9 with no file change)
3210
3199
11
0
0
f66e5f82 (fix round 12, 3426c753, then merges #67's 105f2f12)
3215
3204
11
0
0
a9854078 (in-process local cases get their own state dir), with local and broker settings, a runner OPENDOX_STATE_DIR and a 90-character XDG_STATE_HOME exported
3215
3204
11
0
0
21bde1af (the adversarial review's M1, L1, L2, L3 and replication note)
3236
3225
11
0
0
28e195b9 (fix round 13: the carrier is found as the installed distribution's own files)
3240
3229
11
0
0
57b7ed8f (fix round 14, bf9bcb08, then merges main 66ff7257)
this branch, c04690f8 (fix round 18, 8986158e and c04690f8)
3404
3393
11
0
0
EXPECT_SKIPPED=11 holds exactly, and the floors allow the rise unchanged. The new module adds about 32 s to the run (ten cases, each server start about 1.5 s).
Merging #67's fix rounds.95fe16f merges 32683e8, and 32db5d8 merges 525f61c: runtime migrate and reset refuse what a local install cannot be.
525f61c and this PR both rewrite load_migration_settings. The conflict resolves to this PR's structure: under local, the refusal is asked first, and only then is the bundle's migration DSN read. T070's reason is carried into the comment.
The two merged cases set the local shape as this PR defines it, with the mode and the state dir and no operator DSN. Beside local a DSN is itself refused here, so a case that set one would have tested the DSN refusal instead of the broker or bind refusal it names.
A mutant that drops the refusal from the migration loader fails all 7 merged cases.
ac61596 merges 02dadc5, cleanly. With the runtime extra absent, status's early return now reports a local install's broker as not configured. The merged case uses this PR's local shape and also asserts that database_bundle is reported on that return: present for local, null for hosted.
Fix rounds 4 and 5: Copilot's twelve threads (5e52872, 96b2699f)
Copilot reviewed 95fe16f, 32db5d8 and ac61596a and opened twelve threads. Ten are fixed, answered and resolved. The new cases are in tests_runtime/test_local_lifecycle.py (new, hermetic), plus two in test_bundled_postgres.py.
Migrations (r4139811473, r4139880241).
An explicit OPENDOX_MIGRATIONS_DIR is used as given.
Unset, a local install uses only its own installation's copy, never the working directory's. That copy is the source tree __file__ came from first, then the RECORD of the distribution that holds the running module. Where there is none, it is refused.
A hosted install's default is unchanged (13.6).
The pid (r4139811555, r4139938402).
A pid is believed only when /proc proves it is an executable named postgres running in this data directory. A proven-stale lock is removed before the launch.
Where there is no /proc, nothing is believed, and PostgreSQL's own interlocks stand. The price is a status with no pid on macOS and the BSDs, recorded in the thread.
initdb (r4139880213): it runs into an attempt directory that is renamed into place only on success. Abandoned attempts are removed. A non-cluster data/ is refused and left alone.
start() (r4139880279): every phase is one guarded operation, and every failure is the one named refusal, with its phase and class.
Interrupts (r4139880267): SIGTERM or Ctrl-C anywhere in the local lifecycle is a clean stop, exiting 128 + the signal number. A served run still exits 0.
Refusal wording (r4139880298): broker settings and DSNs are two classes, each with its own reason.
State dir (r4139938444): an unknown ~user, or no home, refuses naming OPENDOX_STATE_DIR.
Test helper (r4139811584): bounded by a selector. With a silent 8 s child and a 1 s deadline, it returned after 1.0 s where the old loop took 8.0 s.
Evidence:
Against ac61596's source, 17 of round 4's first 18 cases fail; the one that passes is the unchanged hosted default. Round 5's cases fail against 28bdccd.
Mutants: 23 of round 4 and 5 of round 5, all killed (runs/mutants-t072-r4.txt, -r5.txt in the writer's workdir).
Round 6 (a0fb7c8d): Copilot's review at 28bdccd9 opened two more threads.
The unknown-_serves point was already fixed at 96b2699f; it is answered and resolved.
The pyproject note named a function that no longer exists. It now names installation_migrations_dir, and the packaging case checks every opendox.runtime.config.<name> pyproject names.
4aed6278 merges T070's 026f00ea, a docstring change.
Round 7 (0f77d5c1): Copilot's review at a0fb7c8d opened three threads, all fixed and resolved. SonarCloud raised one reliability finding.
libpq's environment. Every PG* variable is lifted out of os.environ for the duration and put back afterwards, around generate-and-open --local's lifecycle and the runtime verbs under local. PGHOSTADDR, PGSERVICE and PGOPTIONS can no longer redirect the bundle's connections. The real entry point and status are proven with all three set.
The socket's path. The resolved state tree must be this user's own and writable by no one else. Every ancestor must be owned by the user or root, and sticky where others can write it; a group-writable ancestor of the user's own group is allowed. Symlinks inside the tree are refused before any chmod.
A relative HOME is refused for the default state directory.
SonarCloud S6466 (reliability): server_binaries no longer indexes a list.
Evidence: 8 new cases fail against a0fb7c8d, and 11 mutants are killed.
Round 8 (379fbb14): Copilot's review at 0f77d5c1 opened two threads, both fixed and resolved.
A group-writable ancestor is refused whatever its group, because a primary group can have other members.
The configured path is checked as configured, as well as resolved: both chains' ancestors, every link's owner, and no .. anywhere.
Evidence: 5 new cases fail against 0f77d5c1, and 5 mutants are killed.
Round 9 (84a6c041) applies Brett's two rulings, openxFactory#656 5916000030 items 2 and 3: the carrier and peer authentication, as described above.
The server confirms it, in its own views:
pg_hba_file_rules has exactly the one peer rule (with map=opendox) and the two rejects;
pg_ident_file_mappings has exactly the two mappings, for this OS user;
system_user is peer:<os user> for both roles.
The map decides. The same OS user asking for a role outside the map is refused (peer authentication failed). A non-root suite cannot connect as a second OS user, so for that case the map, read back from the server, stands: it names no other user.
Evidence: 13 new cases fail against 379fbb14. 9 mutants are killed:
initdb trust;
no map;
a trust rule;
a wildcard system user;
a third role;
no re-assertion;
any name admitted;
files 0644;
the old carrier.
The auth mutants are also killed by the real-server cases alone.
Round 10 (f8e6e9e9): Copilot's review at 84a6c041 raised three points, all fixed.
An existing postgres/data joins the tree check, a broken link included (r4147680113, resolved).
Missing parent directories are created exactly 0700 whatever the umask. mkdir(parents=True) under umask 0002 made them group-writable.
Readiness requires the data directory's lock file to name the launched child, so a racing loser cannot adopt the winner's socket.
Evidence: 6 new cases fail against 84a6c041, and 4 mutants are killed.
SonarCloud S2115, ACCEPTED, as ruled.
Issue:AaDvyyOCiqwq-gAw53M3, python:S2115, "Add password protection to this database", on src/opendox/runtime/config.pyDatabaseBundle.dsn.
New status:accept (SonarCloud now reports it RESOLVED). Set with the SonarQube tool on openxFactory#656 5916000030 item 3's authority.
Rationale: the DSN has no password because the server authenticates Unix-socket connections by PEER. The kernel verifies the connecting uid (SO_PEERCRED), and pg_ident.conf maps only this install's OS user to the two roles. The socket directory is 0700, the server has no TCP listener (listen_addresses is empty), and every host connection is rejected. The same rationale is in the DSN's docstring.
Gate: after the change, SonarCloud reports the PR's quality gate OK on every condition.
Phase 2 has landed. This branch now carries #67's c8fac05e, which carries #60's adeb6fed and main 047bb4fa (T054 to T058, T055's follow-up #70 and T056's standalone test). Git auto-merges pyproject.toml (main's validator package data beside this PR's local extra and data files), src/opendox/cli.py and tests/test_doxbench_entrypoint.py without a conflict. Four edits followed, all in 19e32f0c:
The stand-ins in tests_runtime/local_entrypoint_driver.py go. The driver is deleted. Its stand-ins patched names T055 has since replaced, so on the merged tree they stood in for nothing, and all three background cases failed: the real corpus-root check refused the stand-in corpus. test_bundled_postgres.py now runs the real entry point over T050's fixture, with the validator on.
Every cheap refusal comes before the database start. Main's T055 added _refuse_empty_source_options, so the local path asks it before it builds the bundled server. tests/test_projection_seams.py's empty-option case carries a tripwire bundle, so a regression neither starts a server nor passes.
No child touches the user's state directory. T070 gave four phase-2 callers --local, and here --local starts the bundled server, whose OPENDOX_STATE_DIR defaults to the user's ~/.local/state/opendox. tests/standalone_child.py now gives every child a fresh, short, private state directory under /tmp and removes it when the child stops. Measured before: three children of T056 and T058 initialized a cluster in the (sandboxed) default state home.
T056's case 3 asserts it: while serving, its bundled server's data directory is under the child's own state directory, and the directory is gone after the stop.
Mutants, all four killed: the refusal dropped; no private state dir; the dir not removed; the fixture not a repository.
Full suite:3195 selected, 3184 passed, 11 skipped, 0 failed. Nothing is left under ~/.local/state/opendox or /tmp/odx-child-*.
Merge of #67's fix rounds (94254b18, 6eb0bbdb; 2026-10-02)
94254b18 merges #67's cdf7382b, which carries #60's c39d960e (a PostgreSQL scheme libpq would not read as a URI is refused). cdf7382b itself means a --local caller inherits none of the runner's runtime settings. There were two docstring and setup conflicts, and both were resolved by keeping both sides:
tests/standalone_child.py: the code merged cleanly in the needed order. The child's environment first drops every SETTING_NAMES entry, and only then is OPENDOX_STATE_DIR set to the child's own directory.
tests/test_projection_seams.py: the empty-option case scrubs the settings and keeps this PR's bundled-server tripwire.
6eb0bbdb changes #67's harness probe. On #67 it asserted that a child sees no runtime setting. Here every child is given exactly one, its private state directory. The probe now also exports a runner state directory, and asserts three things:
the child sees exactly OPENDOX_STATE_DIR among the runtime settings;
its value is the child's own Child.state_dir, not the runner's;
the directory is gone once the child has exited.
Evidence:
Mutants, all four killed, under an exported hosted install's settings:
the child keeps the runner's settings;
the scrub runs after the state dir is set;
the runner's own state dir is passed through;
the in-process case does not scrub.
Exported settings: the child-driven modules (tests/test_standalone_generate_path.py, tests/test_post_render_validator.py) pass whole with a hosted install's settings and a runner OPENDOX_STATE_DIR exported.
Full suite:3204 selected, 3193 passed, 11 skipped, 0 failed. Nothing is left under ~/.local/state/opendox or /tmp/odx-child-*.
Fix round 11: nothing is made through a path the tree check would refuse (0488f5bd, then fedfa75d)
Copilot's review at 19e32f0c opened one thread, which is real (reproduced) and is now answered and resolved. _prepare_directories made the missing postgres/run before the tree check judged the path, so a component it refuses had already been written through: another user's link, or a 0777 directory. In a sticky parent such as /tmp, another user could also plant the state directory's name between the check and the mkdir. The fix:
What exists is judged before any write._refuse_an_unsafe_tree(existing_only=True) runs the link-ownership loop first, so even a broken foreign link is named. The whole tree is judged again afterwards, before the socket directory's chmod.
Missing components are made by descriptor._make_private_directories makes each one relative to its parent's descriptor and opens it with O_NOFOLLOW. fstat must show it is this user's alone before anything is made beneath it. A planted link, non-directory or foreign directory is the named refusal: never followed, never re-moded.
No mkdir/chmod window. Each component is born 0700 under a umask of 077, and the umask is put back afterwards.
Evidence:
New cases: six, in tests_runtime/test_local_lifecycle.py. All fail against 19e32f0c's bundle.py and pass here.
Mutants: seven, all killed.
Full suite:3210 selected, 3199 passed, 11 skipped, 0 failed.
fedfa75d merges #67's d1de1fd9 (its healthy-local status case reads either DSN form). On this branch that case uses the bundled server, so the conflict resolves to this side and changes no file.
Fix round 12: the auth files are exactly 0600, and the cluster runs on its own files (3426c753, then f66e5f82)
Copilot's reviews at 6eb0bbdb and fedfa75d opened two threads, both real and now answered and resolved.
write_authentication's 0600 was only a creation request. The umask filtered it, and a stale temporary from an interrupted start kept its own mode or was written through as a link. Now the stale temporary is unlinked, the new one is opened O_CREAT | O_EXCL | O_NOFOLLOW, and its descriptor is fchmod-ed to exactly 0600 before anything is written. A link raced in after the unlink is a refusal, never followed.
A reused cluster's postgresql.conf could redirect data_directory, hba_file and ident_file, for example to an outside trust file. The launch now pins all three on the command line, which outranks every configuration file.
Evidence:
New cases: four in tests_runtime/test_local_lifecycle.py (umask, stale 0644, stale link, raced link) and one real-cluster case in tests_runtime/test_bundled_postgres.py. All but the raced-link case fail against fedfa75d.
Mutants: six, all killed.
Full suite:3215 selected, 3204 passed, 11 skipped, 0 failed.
f66e5f82 merges #67's 105f2f12 cleanly. It adds an autouse fixture in tests_runtime/conftest.py that clears every runtime setting before each case, so a case wanting OPENDOX_STATE_DIR sets its own, as every one here already does.
The adversarial review of f66e5f82 (a9854078 to 21bde1af; 2026-10-02)
An adversarial review of f66e5f82 found nothing high. It found one medium, three lows and a note. Each is fixed in its own commit, each with a new case that fails without it and mutants that are killed. One more hermeticity fix came first.
a9854078: the in-process local cases give themselves a short state directory. The doxBench entrypoint fixture and the empty-option case in test_projection_seams.py scrubbed the settings, and so fell back to the runner's default state directory. When that is too long for a Unix socket, configuration refuses it before the case is reached. Measured with a 90-character XDG_STATE_HOME: 4 errors and 1 failure. Each now sets its own short OPENDOX_STATE_DIR and removes it afterwards. Nothing is made in it, because the database is stood in. Both mutants are killed.
M1, medium (e214477d): a comma in the state directory is refused. PostgreSQL splits -k on commas, and libpq splits a decoded host on them. The review reproduced sockets in two unchecked directories, one of them 0777, while the checked 0700 directory stayed empty. config.database_bundle (every bundle's one derivation) now refuses a , from OPENDOX_STATE_DIR, XDG_STATE_HOME or HOME, without repeating the value.
L1 (c4f5df3c): the carrier is pinned to pixeltable-pgserver>=0.6.0,<0.7, and another major is refused by name. Before an existing cluster is used, the server's own postgres --version is asked against its PG_VERSION. Another major, or a server that does not say, is a named refusal, before anything is written. 4 mutants are killed.
L3 (b2d80e94): the directory creation starts from is judged by its descriptor._make_private_directories now fstat-judges the base it opens with _unsafe_because before the first mkdir. That is the install's own rule for the state directory and below, and the ancestors' rule above it. This makes round 11's rule hold inside the function itself. 2 mutants are killed.
L2 (9e2f3030): the local verbs judge their socket before connecting to it.runtime status, migrate and reset connected to whatever answered at the bundle's socket path. Reproduced here: status on a 0777 tree whose run linked to another bundle's socket reported that bundle's applied migrations and exited 0. The fix:
bundle.refusal_before_connecting asks the start's tree check (now bundle.refuse_an_unsafe_tree) of what exists. It then asks for a live server of THIS data directory: its own postmaster.pid must name a live postgres whose working directory is this data directory, listening at this socket directory.
Status's database reads "not probed: <reason>" for a local install that fails this check, including one with no server running. It used to read "unreachable: …", found by connecting.
migrate and reset refuse as local-bundle-unverified. Hosted is unchanged.
The status database block is re-indented under the new branch; git diff -w shows only the branch.
7 mutants are killed.
The note (21bde1af): no replication connection, logical or physical. Fixed, not only reworded.authentication_files' docstring said a replication connection is refused. A physical one was refused, but a logical one (replication=database) was accepted as peer:<user>, and IDENTIFY_SYSTEM answered (measured). No pg_hba.conf rule can tell it from an ordinary connection. So the launch sets max_wal_senders=0, and the docstring now says both kinds are refused by the server. The mutant is killed.
Full suite:3236 selected, 3225 passed, 11 skipped, 0 failed. Nothing is left under ~/.local/state/opendox or /tmp/odx-*.
Fix round 13: the server is found as the installed distribution's own files (28e195b9)
Copilot's review at f66e5f82 opened one thread, which is real and is now answered and resolved. server_binaries used importlib.util.find_spec, which follows sys.path. Under python -m opendox.cli that starts with the working directory, and a corpus checkout is where it runs. So a checkout holding an executable pixeltable_pgserver/pginstall/bin/postgres was run as the database server.
The carrier is now looked up by distribution name (importlib.metadata), on sys.path without the working directory. Both binaries must be files its RECORD lists, inside it and executable.
Evidence:
New case: the working directory holds an importable package and a forged .dist-info, and the server is not taken from it.
Reworked lookup case: four refusal shapes.
Before: 6 of the 8 fail against find_spec.
Mutants: 4 killed, 1 equivalent.
Full suite:3240 selected, 3229 passed, 11 skipped, 0 failed.
Fix round 14: a named platform gate, resolution failures as reasons, no pid behind a refused tree (bf9bcb08)
Copilot's review at 21bde1af opened three threads, all real and now answered and resolved.
A named platform gate. The bundle is a POSIX design, but the carrier ships Windows wheels, where a start failed as an AttributeError. bundle.unsupported_platform() names the missing primitives (os.getuid, O_DIRECTORY, O_NOFOLLOW, os.fchmod, mkdir with dir_fd, socket.AF_UNIX). A start and the local verbs' socket check ask it first.
Resolution failures are reasons.refusal_before_connecting() names a symlink loop (RuntimeError) and an embedded NUL (ValueError) as reasons, beside OSError.
No pid behind a refused tree.bundle.report() reports a pid only behind a verified tree. A postgres/data linked to another live bundle had reported that server's pid.
Evidence:
New cases: three hermetic ones, and a linked-data shape with null-pid assertions in the real verbs case.
Mutants: eight, all killed.
Merge of main after #67 landed (57b7ed8f; 2026-10-03)
#67 (T070) landed as a squash, 66ff7257, after #61 (T078, 8a98e317). Main thus holds T070's content as one commit this branch's history never saw, and a plain merge against the old base 047bb4fa conflicted in ten files where both sides carry the same T070 text.
So the merge is computed against the T070 state this branch already held, #67's 105f2f12: git merge-tree --write-tree --merge-base=105f2f12 HEAD origin/main. It is recorded with both parents. The base is sound because git diff 25262459 66ff7257 (#67's last branch head against its squash) names only #61's three files. Against it the merge is clean:
17 of them are byte-identical to main's.src/opendox/cli.py is the one merged file: main's two doxbench_defaults registrations sit beside this branch's local path.
Full suite:3327 selected, 3316 passed, 11 skipped, 0 failed.
Fix round 15: on Linux the parent-death signal is armed, or the server is not started (058d96ef)
Copilot's review at 57b7ed8f opened one thread, which is real and is now answered and resolved. ctypes reports a failed prctl() by returning -1, never by raising (a seccomp denial, say), and the child ignored it. A killed entry point could then orphan the server, against R1Q16 (iv).
The fix:
A failed prctl is a refusal. The child now raises on a nonzero return. subprocess re-raises that as SubprocessError, which _launch names as the refusal "could not be given its parent-death signal", with no server process left.
A Linux C library with no prctl is the same refusal, where it used to fall back silently. Other platforms are unchanged.
Evidence:
New case (Linux): a stand-in prctl that returns -1. The start refuses by name, and the stand-in server never runs.
Mutants: both killed.
Full suite:3328 selected, 3317 passed, 11 skipped, 0 failed.
Merge of main after #62 landed (085ba0b0; 2026-10-03)
085ba0b0 merges main 2fc714d2 (#62, T079). #62 changes only doxbench_binding.py, doxbench_intake.py, doxbench_provider.py and test_model_provider_broker.py, none of which this branch touches, so the merge is clean. It runs no generate-and-open child, so it needs no --local. Full suite: 3346 selected, 3335 passed, 11 skipped, 0 failed.
Fix round 16: a local install's served role and database are the bundle's own (3ebccb3c)
Copilot's review at 085ba0b0 opened one thread, which is real and is now answered and resolved. Under local, OPENDOX_RUNTIME_PG_ROLE could replace the bundle's served role in the settings. The migration run narrows the ledger privileges of exactly that configured role, while the bundle bootstraps opendox_runtime with the default DML and its served DSN connects as opendox_runtime. So OPENDOX_RUNTIME_PG_ROLE=pg_read_all_data left opendox_runtime able to rewrite opendox_schema_migrations.
refuse_what_a_local_install_cannot_be (asked by both loaders and by generate-and-open --local) now accepts two settings only unset or naming the bundle's own:
OPENDOX_RUNTIME_PG_ROLE must be opendox_runtime;
OPENDOX_SERVED_DATABASE must be opendox, since it declares the same identity.
Hosted is unchanged.
Evidence:
New case: both settings, through both loaders.
Mutants: both killed.
Full suite:3350 selected, 3339 passed, 11 skipped, 0 failed.
Merge of main after #74 landed (52a2b8dc; 2026-10-03)
52a2b8dc merges main 9a490405 (#74, T081), with no overlap. #74's standalone generate-and-open fixture already passes --local, since it landed after #67. Under T072 that child now starts a bundled server in the harness's private state directory.
Full suite:3399 selected, 3388 passed, 11 skipped, 0 failed.
Fix round 17: the /proc falsifier is gated, and a backslash in the map is pinned literal (4a9dbe95)
Copilot's review at 52a2b8dc opened two threads, both now answered and resolved.
The /proc falsifier is gated.test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener reads Linux's /proc, so it now skips where /proc is absent, as the other /proc and PDEATHSIG cases do. On Linux, and in CI, nothing changes.
Backslashes are pinned literal, not escaped. The thread suggested escaping backslashes in pg_ident.conf. Measured against the bundled PostgreSQL 16.14, pg_ident_file_mappings reads "DOMAIN\alice", "alice\" and "a\\b" back as exactly those names, without error. The tokenizer treats a backslash specially only at the end of a line, never inside quotes, so escaping would map a different name.
The code is unchanged.
A real-server read-back case and a hermetic verbatim assertion pin the behaviour.
The escaping mutant is killed.
Full suite:3402 selected, 3391 passed, 11 skipped, 0 failed.
main has since taken #63 (T080, 1130e996), whose five files (the doxbench model-provider family) do not overlap this branch. The PR stays MERGEABLE, and CI's merge ref includes it.
Fix round 18: one schema for both DSNs, and an uninspectable live pid fails closed (8986158e, c04690f8)
Copilot's review at 4a9dbe95 opened two threads. The holder accepted both, and both are now answered and resolved.
8986158e: both bundle DSNs name search_path=public. Left implicit, PostgreSQL's "$user", public let a reused cluster with a schema named opendox or opendox_runtime split the owner's ledger from the served role's reads.
New real-server case: both role-named schemas are created, then the bundle restarts. Both roles' current_schema() is public, they read one ledger, nothing is re-applied, and status is reachable with nothing pending.
Before: without the pin, the restart fails with RuntimeAccessMissingError.
Mutants: 2 killed.
c04690f8: a live pid /proc will not describe is unknown, not "not ours"._identity raises _Withheld for anything but a vanished entry, and _remove_a_proven_stale_lock keeps the lock and refuses the start by name rather than unlinking a possibly live server's lock. With no /proc at all, PostgreSQL still judges.
New case: a live pid whose /proc links are withheld. It fails against 4a9dbe95.
Mutants: 2 killed.
Full suite:3404 selected, 3393 passed, 11 skipped, 0 failed, run with LANG=C.UTF-8 and no GIT_*/XF_*.
Done at the merge round: the driver's stand-ins are gone, and the probe runs the real entry point on tests/fixtures/plain-documents.
For T073 and T075, which stack on this PR: a --local child built with tests/standalone_child.py gets its own state directory automatically. Any other --local caller must set OPENDOX_STATE_DIR itself.
T073 reads database_bundle from the server object the entry point keeps (args.database_bundle) for /capabilities' install block.
Add a self-contained local installation mode that owns, migrates, reports, and cleans up its bundled PostgreSQL server.
New Features:
Add a standalone local-install mode with a bundled PostgreSQL server managed by the document entry point.
Expose bundled database location and process information through runtime status, including separate migration and serving credentials over a Unix socket.
Package migrations and the PostgreSQL carrier in local-install wheels so installations outside a checkout can initialize and migrate their database.
Bug Fixes:
Prevent local installs from using operator-supplied database or migration DSNs and from accidentally applying migrations from another working directory.
Ensure stale or conflicting bundled-server state is detected safely and refuse multiple entry points sharing one state directory.
Enhancements:
Enforce a state-directory layout, absolute-path and Unix-socket length constraints, local-only socket access, and no TCP listener.
Manage bundled PostgreSQL lifecycle with child-process ownership, graceful shutdown, parent-death cleanup, and bounded startup failure handling.
Resolve local and hosted migration sources independently while preserving the hosted migration default.
Build:
Add the local and test dependency extras, pin bundled-server dependencies, and include migrations in built wheels.
Deployment:
Document OPENDOX_STATE_DIR in the compose environment example.
Tests:
Add end-to-end coverage for bundled PostgreSQL startup, migration, process ownership, lifecycle cleanup, socket security, refusal behavior, and wheel installation outside a checkout.
Extend install-mode and runtime-surface tests for local settings, bundled database reporting, packaging, and lifecycle interrupt handling.
…a collapsed DSN pair (plan 034)
Realizes #1144 13.2 and 13.3, falsifier F13.1's `load_settings` block.
- 13.2: `_refuse_non_postgresql_dsn` refuses either DSN (`OPENDOX_DATABASE_URL`
or `OPENDOX_MIGRATION_DATABASE_URL`) whose URI scheme is not `postgresql://`
or `postgres://`, naming the setting and the dialect kept. The keyword/value
conninfo form (`host=h dbname=d …`) names no dialect at all and is
unaffected — that syntax is libpq's own grammar, and no other driver reads
it.
- 13.3: `OPENDOX_MIGRATION_DATABASE_URL` stops being optional in
`load_settings` (the `Setting` row's `required` flag, `_require` in place
of `_optional`, and `RuntimeSettings.migration_database_url`'s type). A new
`_refuse_the_same_dsn_in_both_settings` refuses the two DSNs being the exact
same STRING, naming `OPENDOX_MIGRATION_DATABASE_URL`, once they are already
known to agree on where they land
(`_refuse_two_dsns_that_select_different_schemas`, unchanged, now called
first): two DIFFERENT secrets for one role still pass, as the existing
"single-role install" case documents.
- Explicitly NOT in this task: 13.4-13.6 (`OPENDOX_INSTALL_MODE`, T070).
Nothing here reads or names that setting, and `load_settings`'s only new
required input is the migration DSN itself.
Every existing call site that built an environment without
`OPENDOX_MIGRATION_DATABASE_URL` needed one once it became required:
`tests_runtime/conftest.py` gains a `migration_dsn` fixture (a `postgres_dsn`
distinguished by a URI fragment, invisible to every DSN reader this module
has); `test_api_endpoints.py`, `test_migrations_apply.py`,
`test_runtime_cli.py` and `test_runtime_surface.py` thread it or a literal
peer through. `test_two_dsns_that_select_different_schemas_are_refused`'s
"a migration DSN that is simply absent" case is rewritten from accepted to
refused, which is the behavior 13.3 changes. Two new tests
(`test_a_non_postgresql_dsn_is_refused_naming_the_dialect_kept`,
`test_the_same_dsn_in_both_settings_is_refused_naming_the_migration_one`)
cover the two new refusals directly.
Measured locally against this change (own Postgres container, bridge IP —
this sandbox's host-mapped loopback ports are unreachable): `python -m
pytest -q` reports 2469 passed, 11 skipped, 1 failed — the one failure is
`tests/test_model_provider_broker.py::test_the_broker_child_inherits_no_
credential_shaped_environment`, already red against unmodified `main`
(2d11641) in the same environment (an `LC_CTYPE` ambient in this sandbox,
unrelated to runtime/config.py). Against `main`'s own reading (2479
selected / 2468 passed / 11 skipped, matching this repo's last recorded CI
triple), this change is +2/+2/+0 for the two new tests — `validate.yml`'s
`Pin the triple` floors (`MIN_SELECTED=2476`, `MIN_PASSED=2465`,
`EXPECT_SKIPPED=11`) permit the rise unchanged.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…(Copilot review of this PR)
`urlsplit` itself raises for a DSN it cannot parse — MEASURED,
ValueError("Invalid IPv6 URL") for an unbracketed IPv6 host, which
tests_runtime/conftest.py's own postgres_dsn docstring names as "the
ordinary way to mis-set this variable". `_refuse_non_postgresql_dsn` called
`urlsplit(dsn).scheme` unguarded, so that ValueError escaped load_settings
as a bare exception instead of the promised ConfigurationError — the CLI's
boundary catches only ConfigurationError, so a malformed OPENDOX_DATABASE_URL
or OPENDOX_MIGRATION_DATABASE_URL would have printed a traceback instead of
a redacted refusal.
Wrapped the same way _split_url already wraps it for the broker settings
(Copilot review of openDox-code#25, round 24), with DSN-appropriate wording
rather than reused verbatim ("set it to the broker endpoint" does not fit
a database DSN). New test
test_an_unparseable_dsn_is_refused_and_never_raises_a_bare_valueerror
proves both DSNs are covered and that the value is never repeated in the
message.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… only for migrate
Brett ruled on the held conflict (openxFactory#656, on the claim thread for
plan 034's T071, 2026-09-28), choosing "Required only for migrate
(Recommended)" over making the setting required everywhere:
- OPENDOX_MIGRATION_DATABASE_URL goes back to OPTIONAL in `load_settings`
(the `Setting` row's `required` flag, `RuntimeSettings.migration_
database_url`'s type back to `str | None`, `_optional` in place of
`_require`). `load_migration_settings` is unaffected either way — it
already independently required one, for `migrate`/`reset` alone.
- Both refusals from the previous commits stay, and are now no-ops on an
ABSENT migration DSN rather than being unreachable: `_refuse_non_
postgresql_dsn` and `_refuse_the_same_dsn_in_both_settings` each return
early when the migration value is falsy, exactly the way `_refuse_two_
dsns_that_select_different_schemas` already treated "nothing to compare"
as nothing to fault. When BOTH are given, every check still runs, in the
same order as before (dialect, then schema-mismatch, then collapse).
It is never defaulted from OPENDOX_DATABASE_URL.
- This matches #1144 13.3's own text and `deploy/compose/docker-compose.
yaml`'s separation (the `opendox` service never gets a migration DSN;
`docs/runtime.md` § 3 never lists it as required) — neither file needed
a change; both already said the now-ruled behavior. The plan's "stops
being optional" line is a holder-side correction, not part of this PR,
and #1144's own wording is unchanged.
Reverted the 27-call-site ripple the `required` flip had forced, now that
it is not needed: `tests_runtime/conftest.py`'s `migration_dsn` fixture is
gone; `test_api_endpoints.py`, `test_migrations_apply.py`, `test_runtime_
cli.py` and `test_runtime_surface.py` are back to threading only the
served DSN through every call site that does not itself test the
migration path. All four files after conftest.py are byte-for-byte
`main` again. `test_two_dsns_that_select_different_schemas_are_refused`'s
"absent migration" case is back to ACCEPTED (with a note on why it was
briefly the opposite), which is what the setting being optional again
means for that test.
Added three tests showing the ruled behavior, at the CLI dispatch level
rather than only `load_settings` directly, next to the existing `migrate`
counterpart:
- `test_serve_and_status_load_with_no_migration_dsn_configured`: `status`
reports no configuration refusal and `settings[…MIGRATION_DATABASE_URL]
` as `null` with only the served DSN set; `serve` starts (`ok: true`)
the same way.
- `test_the_collapse_is_refused_through_the_served_workload_too`: 13.3's
collapse refusal still fires through `status`, not only through
`load_settings` called directly, the moment both DSNs are given and are
the same value.
- `test_migrate_refuses_rather_than_borrowing_the_served_identity`
(pre-existing, untouched) already covers "migrate refuses without it".
Measured locally against this change (own Postgres container, bridge
IP): `python -m pytest -q` reports 2472 passed, 11 skipped, 1 failed —
the one failure is the same `tests/test_model_provider_broker.py::
test_the_broker_child_inherits_no_credential_shaped_environment` LC_CTYPE
sandbox artifact already characterized as pre-existing and unrelated in
the first commit on this branch. Against main's 2479 selected / 11
skipped in this same environment, this change is +5/+5/+0 (five tests:
the three already on this branch plus the two new ones above) —
`validate.yml`'s `Pin the triple` floors (`MIN_SELECTED=2476`,
`MIN_PASSED=2465`, `EXPECT_SKIPPED=11`) permit the rise unchanged, and
the exact skip count is unchanged.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ttings has
Copilot review of this PR (thread on _refuse_non_postgresql_dsn's own
definition): 13.2's dialect gate was wired into `load_settings` only.
`load_migration_settings` — the loader `runtime migrate`/`reset` actually
use — read OPENDOX_MIGRATION_DATABASE_URL, checked only that it was
non-empty, and handed it straight to `Database`, so a non-PostgreSQL
migration DSN (`sqlite:///x.db`, say) reached the driver instead of being
refused by name at configuration. That is the same un-named failure 13.2
exists to prevent for the served loader, just reachable through the one
path F13.1's falsifier does not call.
One call to the existing `_refuse_non_postgresql_dsn`, right after the
existing empty-DSN refusal and before `database_url`/`migration_database_
url` are both set to the same value. New test
`test_migrate_refuses_a_non_postgresql_migration_dsn_at_configuration`
is the dialect-refused twin of the existing `test_migrate_and_reset_need_
no_served_identity_and_no_broker`, which already shows an unreachable but
valid-dialect migration DSN getting PAST configuration — this one shows a
wrong-dialect one refused AT configuration, naming the setting and never
repeating the DSN.
Measured locally (own Postgres container, bridge IP): 2473 passed (+1),
11 skipped, 1 failed (the same pre-existing, unrelated LC_CTYPE sandbox
artifact) — the new test is the only change to the count.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n --local (plan 034)
OPENDOX_INSTALL_MODE (`local` | `hosted`, default `hosted`) is read in
runtime/config.py beside OPENDOX_OIDC_ISSUER and decides the install shape
(#1144 13.4). `generate-and-open --local` makes the same selection
(R1Q15 (b), as T007 batch H's 13.4 addendum reads); with neither the install
is hosted (13.5).
- A flag and a setting that disagree (`--local` beside
OPENDOX_INSTALL_MODE=hosted) are refused, naming both. This is plan 034's
fail-closed reading (Principle VII); no answer rules it and batch H does
not write it into #1144.
- LOCAL needs no broker: issuer, audience and key-set URL are empty.
- LOCAL binds loopback only, with no opt-in. A non-loopback `--host` or
OPENDOX_BIND_HOST is refused, naming the rule. The set is serve.py's own
LOOPBACK_HOSTS, and a test holds the two equal.
- HOSTED, set or by default, with no issuer refuses, naming
OPENDOX_OIDC_ISSUER. generate-and-open asks the issuer first, so a run with
nothing configured names it and `--local`. The hosted mode is otherwise
unchanged (13.6).
Holder readings on openxFactory#656 (Brett may overrule):
- `runtime serve` refuses under local, because the API's identity is the
broker's.
- `runtime status` under local reports broker_keys "not configured (local
mode)" and does not count it as a fault.
- A broker setting beside local is refused by name.
- An unrecognised mode value is refused, case-sensitively.
The document server's generate-and-open resolves the shape before it scans,
mints or binds anything. The hosted path loads the whole runtime
configuration (R1Q16 (i); 13.4a).
Also:
- deploy/compose/.env.example gains OPENDOX_INSTALL_MODE=hosted, which
test_every_runtime_setting_is_documented_in_env_example requires of every
SETTINGS entry.
- tests/test_doxbench_entrypoint.py's fixture now selects `--local` and
scrubs the runtime settings, since the unset default is hosted and refuses
with no issuer.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hild (plan 034)
A LOCAL install (T070's `generate-and-open --local`, or
OPENDOX_INSTALL_MODE=local) now brings its own database (#1144 13.1, as
T007 batch H's addendum reads; RULED R1Q16 (i)-(iv), 5850003126).
- (i) `generate-and-open --local` starts a PostgreSQL server as its own
direct child (subprocess.Popen, never pg_ctl) and reports it. The
document server a user reaches is the process that owns it.
- (ii) The server is started AND migrated: initdb once, an idempotent
bootstrap (the database, the served role, and the compose stack's grants
narrowed to this install's owner), then migrations.MigrationRunner as the
owner, with the served role and database declared.
- (iii) It ships as the `opendox[local]` extra: `opendox[runtime]` plus
`pgserver>=0.1.4`, whose bundled binaries link only libc and libz. The
`test` extra joins it, so F9.1's `.[test]` install still runs every case.
- (iv) It stops with the entry point. SIGTERM is read as the Ctrl-C the
serve loop already stops on, followed by a fast shutdown.
PR_SET_PDEATHSIG is the backstop when the entry point is SIGKILLed.
- Its data and socket directories live under OPENDOX_STATE_DIR, a new
setting that defaults per user and must be absolute. The server listens
on a 0700 Unix socket with listen_addresses empty: no TCP listener at all.
- Both DSNs are supplied: two users over the one socket, which pass T071's
three checks. An operator DSN beside `local` is refused by name, joining
T070's broker settings (a holder reading on openxFactory#656).
- `runtime status` reports database_bundle (data_dir, socket_dir, pid).
`runtime migrate` under local migrates the bundle.
THE MIGRATIONS GAP (assigned to T072 by the holder). pyproject maps
migrations/*.sql into the wheel's data directory (share/opendox/migrations),
without moving the root migrations/ that the image copies. An unset
OPENDOX_MIGRATIONS_DIR is `migrations` wherever the working directory has
one (today's default, unchanged), and otherwise the copy the installed
distribution records. A test builds the wheel, installs it outside the
checkout, runs from a directory with no migrations/, and migrates the
bundled server.
Also:
- deploy/compose/.env.example gains OPENDOX_STATE_DIR=, because every
SETTINGS entry is named there.
- tests/test_doxbench_entrypoint.py stands the bundle in, since those cases
test the model port.
- T070's own tests stop passing DSNs beside `local`.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…xtra's setuptools
The lock is extended under its own pins (`-c` this file), in a clean
cpython 3.12.3 venv on linux x86_64, as its header asks. Five pins are new:
- pgserver 0.1.4, with its own psutil, platformdirs and fasteners;
- setuptools, for the wheel-install test's offline build.
No earlier pin moved.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it config (Copilot review)
`tests/test_install_mode_entrypoint.py`'s `corpus` fixture ran `git commit`
under the caller's global and system git configuration. A global
`commit.gpgsign=true` therefore failed the setup before any install-mode
probe ran. Measured with a hostile global config (`commit.gpgsign = true`,
`gpg.program = /bin/false`): 7 errors at b50e3b1, 14 passed here. The
fixture now sets GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1, as
tests/test_checkout_head.py does.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This PR makes local installation mode self-contained by packaging PostgreSQL binaries and migrations, deriving a private Unix-socket database under OPENDOX_STATE_DIR, managing the server as the document process's child through startup, migration, status reporting, and shutdown, and adding integration and wheel-install tests that falsify the process, security, packaging, and lifecycle guarantees.
Sequence diagram for the local bundled PostgreSQL lifecycle
sequenceDiagram
participant User
participant CLI as opendox CLI
participant Bundle as BundledServer
participant Postgres as postgres child
participant Runner as MigrationRunner
User->>CLI: generate-and-open --local
CLI->>Bundle: start()
Bundle->>Bundle: server_binaries()
Bundle->>Bundle: _initdb()
Bundle->>Postgres: subprocess.Popen()
Bundle->>Postgres: wait for readiness
Bundle->>Bundle: _bootstrap()
Bundle->>Runner: apply()
Runner-->>Bundle: applied migrations
Bundle-->>CLI: report()
CLI-->>User: serve document surface
User->>CLI: SIGTERM or Ctrl-C
CLI->>Bundle: stop()
Bundle->>Postgres: SIGINT fast shutdown
Bundle->>Postgres: SIGQUIT if needed
Bundle->>Postgres: SIGKILL if needed
Loading
Sequence diagram for local runtime status reporting
sequenceDiagram
participant Operator
participant RuntimeCLI as runtime status
participant Config as runtime.config
participant PID as postmaster.pid
Operator->>RuntimeCLI: runtime status
RuntimeCLI->>Config: load_settings()
Config-->>RuntimeCLI: state_dir and local settings
RuntimeCLI->>Config: database_bundle(state_dir)
RuntimeCLI->>PID: read pid
PID-->>RuntimeCLI: postgres pid or none
RuntimeCLI-->>Operator: database_bundle data_dir, socket_dir, pid
Loading
File-Level Changes
Change
Details
Files
Add a bundled PostgreSQL lifecycle for local installs, owned directly by the document-serving process.
Resolve a per-install state directory and fixed Unix-socket layout.
Start PostgreSQL with subprocess.Popen, no TCP listener, socket permissions, parent-death handling, and bounded shutdown escalation.
Bootstrap roles/database, run migrations, expose bundle metadata, and reject concurrent owners.
Expand tests and runtime falsification around local bundled-server guarantees.
Add background entry-point tests for migration, process ancestry, shutdown, socket permissions, no TCP listeners, SIGKILL cleanup, and duplicate-server refusal.
Add packaging/wheel installation coverage outside a checkout and update local-mode/configuration tests and import-surface contracts.
Use a narrowly scoped generation/serve stand-in until the stacked upstream changes land.
Trigger a new review: Comment @sourcery-ai review on the pull request.
Continue discussions: Reply directly to Sourcery's review comments.
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with @sourcery-ai issue to create an issue from it.
Generate a pull request title: Write @sourcery-ai anywhere in the pull
request title to generate a title at any time. You can also comment @sourcery-ai title on the pull request to (re-)generate the title at any time.
Generate a pull request summary: Write @sourcery-ai summary anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment @sourcery-ai summary on the pull request to
(re-)generate the summary at any time.
Generate reviewer's guide: Comment @sourcery-ai guide on the pull
request to (re-)generate the reviewer's guide at any time.
Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore.
Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment @sourcery-ai review to trigger a new review!
…ot be (Copilot review)
load_migration_settings recorded OPENDOX_INSTALL_MODE=local but never asked
refuse_what_a_local_install_cannot_be. So `runtime migrate` and a
confirmed `runtime reset` accepted OPENDOX_OIDC_ISSUER, OPENDOX_OIDC_AUDIENCE,
OPENDOX_OIDC_JWKS_URL or a non-loopback OPENDOX_BIND_HOST beside `local`,
which load_settings and generate-and-open both refuse. They now refuse them
at configuration, before any database is reached.
Seven new cases:
- the three broker settings x {migrate, reset};
- the bind.
All seven fail at 32683e8 and pass here. Full suite: 2538 selected, 2527
passed, 11 skipped, 0 failed.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
T070's 525f61c makes `load_migration_settings` refuse what a local install
cannot be (Copilot review of openDox-code#67). T072 had already restructured
the same lines: under `local`, it asks that refusal and then takes the
bundle's migration DSN. The conflict resolves to T072's structure, with
T070's reason carried into its comment. The refusal is asked once, before
the bundle's DSN is read.
The two merged cases now set the local shape as T072 defines it, with the
mode and the state dir and no operator DSN. Beside `local` a DSN is itself
refused (T072), so a merged case that set one would have tested the DSN
refusal rather than the broker or bind refusal it names.
Full suite: 2552 selected, 2541 passed, 11 skipped, 0 failed. A mutant that
drops the refusal from the migration loader fails all 7 merged cases.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n too (Copilot review)
When the runtime extra is absent, `runtime status` returns early, and that
return said `broker_keys: "not probed"` for every install. A local install's
broker is not configured whether or not the extra is present. That answer
comes from the configuration, not from a probe, so the early return now gives
the local install the answer the full report gives: `"not configured (local
mode)"`, with `broker_discovery: null`. Both returns write it through one
helper, so the two cannot drift. A hosted install's early return still reads
"not probed", as before (13.6).
The branch is covered now, so its `pragma: no cover` goes. A new case runs
both shapes with `opendox.runtime.db` absent from `sys.modules`. Before
(`525f61c`'s runtime/cli.py): local 1 failed and hosted passed. After: both
pass. Four mutants of the fix are killed. Full suite: 2540 selected, 2529
passed, 11 skipped, 0 failed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
T070's 02dadc5 makes `runtime status`, when the runtime extra is absent,
report a local install's broker as not configured on the early return too
(Copilot review of openDox-code#67). It merges cleanly: T072's
`database_bundle` report comes before that return, in another hunk.
The merged case sets the local shape as T072 defines it, with the mode and
the state dir and no operator DSN. It also asserts that the bundle is
reported on the early return (`database_bundle` present for local, `null`
for hosted).
Full suite: 2554 selected, 2543 passed, 11 skipped, 0 failed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tings' broker invariants are scoped (Copilot review)
A local `status` returns `ok` on the database's verdict alone. Both earlier
local cases forced a database fault and asserted exit 1, so a regression
that also counted the absent broker as a fault would still have passed. A
DB-backed case now runs `status` for a local install against a migrated
schema on the suite's own server (`database` and `postgres_dsn`, with the
schema selected in the DSN). It asserts `ok` true, exit 0, the database
reachable with nothing pending and no drift, and the broker reported as not
configured and never probed. Measured: with the local return mutated to
`ok=False`, this case fails and the other 40 in the module pass.
`RuntimeSettings`' docstring said that a local install's issuer and audience
are empty and that a hosted one always carries a real issuer. That is true of
`load_settings` alone. `load_migration_settings` carries the migration
sentinels in either shape. The docstring now scopes each statement to its
loader.
Full suite: 2541 selected, 2530 passed, 11 skipped, 0 failed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…errupts hardened (Copilot review)
Copilot's reviews at 95fe16f and 32db5d8 opened ten threads. Eight are fixed
here. The two about the server package (PostgreSQL 16.2, no wheel for 3.13)
wait on the holder.
- Migrations (r4139811473, r4139880241). An explicit OPENDOX_MIGRATIONS_DIR
is used as given. Unset, a LOCAL install uses only the copy its own
installation carries, and never the working directory's: its entry point
runs every migration as the bundle's owner, and the canonical gate pins
0001 alone. Where the installation carries none, it is refused, naming the
setting. The installation's copy is the source tree the module was
imported from (src/ beside a pyproject.toml naming opendox), then the
RECORD of the distribution that holds the running module, and never
another one found by name. A HOSTED install's unset default is unchanged
(13.6).
- The pid (r4139811555). A postmaster.pid is believed only for this data
directory's postmaster, as the kernel reports it: an executable named
postgres whose working directory is the data directory. Another user's
process is never believed. A lock that /proc proves stale is removed
before the launch, so a recycled pid no longer holds the bundle.
- initdb (r4139880213). It runs into an attempt directory beside the data
directory, which is renamed into place only on success. An attempt whose
process is gone is removed. A non-empty data directory that holds no
cluster is refused and left untouched.
- start() (r4139880279). Directories, initialize, launch, wait, bootstrap
and migrate are one guarded operation, and every failure is the one named
refusal (phase and class name), with anything started stopped.
- Interrupts (r4139880267). SIGTERM or Ctrl-C anywhere in the local
lifecycle is a clean stop: no traceback, the bundle stopped, the handler
restored first. Nothing was served, so the exit is 128 + the signal number.
A served run ended by SIGTERM still exits 0.
- Refusal wording (r4139880298). Broker settings and operator DSNs are two
classes, and each is refused with its own reason.
- The test helper (r4139811584). The launch helper is bounded by its
deadline, through a selector. Measured with a silent 8 s child and a 1 s
deadline: the old loop returned after 8.0 s, the new one after 1.0 s.
tests_runtime/test_local_lifecycle.py (new, hermetic) holds these cases,
plus a real-server stale-lock case and the helper's own case in
test_bundled_postgres.py. Against ac61596's source, 17 of the module's
first 18 cases fail. The one that passes is the hosted default, which is
unchanged on purpose. All 23 mutants of the fixes are killed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
T070's 859b37b adds a DB-backed case proving that a healthy local
`status` exits 0, and scopes RuntimeSettings' broker invariants to their
loader (Copilot review of openDox-code#67). It merges cleanly.
Here the case uses the local install's own database. Beside `local` an
operator's DSN is refused (T072), so the case starts the bundled server
on a fresh state directory and asks `status` about it. With the local
return mutated to `ok=False`, it fails and the other 42 cases in the
module pass.
Full suite, with this PR's fourth fix round (5e52872): 2580 selected,
2569 passed, 11 skipped, 0 failed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es by name (Copilot review)
Copilot's review at ac61596 opened two more threads, both real.
- r4139938402: the old fallback believed any pid it could not inspect. So a
process that exited between the signal check and the /proc read, or any
pid on a platform without /proc, counted as the server. Round 4 already
treated a vanished process as gone on the /proc path. This round makes
the rule total. `running_pid` believes a pid only when the kernel proves
it is this data directory's postmaster. Where nothing can be asked (no
/proc: macOS, the BSDs), it believes nothing, and this module does not
refuse a start over it. PostgreSQL's own interlocks, the lock file's
live-pid check and the shared-memory check, still refuse a second
postmaster, so this never yields two servers, and never a refusal over a
process that is not one. A lock that cannot be proven stale is left for
PostgreSQL to judge. The price on such a platform is a `status` with no
pid. That is recorded, not hidden: the standard library has no portable
way to ask, and a third-party module here would be an undeclared runtime
dependency (test_consumer_reach). Measured before: round 4's source with
no /proc, and a python decoy in the data dir, reported the decoy's pid.
ac61596's source reported a pid that had already exited.
- r4139938444: `Path.expanduser()` raises RuntimeError for an unknown
`~user`, and `Path.home()` does the same where there is no home. Both now
refuse by name, as ConfigurationError naming OPENDOX_STATE_DIR. A hosted
install still never reads the setting and is not refused over it (13.6).
Five new cases fail against 28bdccd's source and pass here. Five mutants of
the fixes are killed. Full suite: 2584 selected, 2573 passed, 11 skipped,
0 failed.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fc714d#69 moved from 058d96e to 085ba0b. It brings round 15's refusal: a failed
prctl(PR_SET_PDEATHSIG) is refused by name. It also brings stack A's own
merge of main 2fc714d (#62 T079). Merged, never rebased.
There were no conflicts. The whole suite: 3176 passed, 177 skipped.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Restore termination handlers only after shutdown completes
src/opendox/cli.py:704
Restoring the previous SIGTERM disposition before server.stop() leaves the shutdown unprotected. A second SIGTERM (or another Ctrl-C, whose handler is never masked) can terminate the entry point while the bounded PostgreSQL shutdown is still running; on non-Linux POSIX systems there is no parent-death signal, so PostgreSQL can outlive the entry point, violating lifecycle guarantee (iv). Keep termination signals blocked/ignored until stop() completes, then restore the prior handlers.
T073 merged #69's 058d96e. That brings stack A's fix rounds 14 and 15, and
stack A's own merge of main 66ff725 (#67 T070, #61 T078). Merged, never
rebased.
There were no conflicts. The whole suite: 3231 passed, 177 skipped.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
T073 merged #69's 085ba0b, which brings round 15's named
PR_SET_PDEATHSIG refusal and stack A's merge of main 2fc714d (#62 T079).
Merged, never rebased.
There were no conflicts. The whole suite: 3249 passed, 177 skipped.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…p is pinned literal (Copilot review)
Two threads from Copilot's review at 52a2b8d.
- test_the_entry_point_owns_a_migrated_server_with_no_tcp_listener reads
Linux's /proc for the server's parent, its TCP listeners and status's
pid. The bundle runs on any POSIX platform, so it is now skipped where
/proc is absent, as the other /proc and PDEATHSIG cases already are. On
Linux, and in CI, nothing changes.
- The second thread suggested escaping backslashes in pg_ident.conf's
quoted user field. Measured against the bundled PostgreSQL 16.14, that
would be wrong. pg_ident_file_mappings reads `"DOMAIN\alice"`,
`"alice\"` and `"a\\b"` back as exactly those names, with no error.
PostgreSQL's tokenizer treats a backslash specially only at the end of a
line, as a continuation, never inside quotes. So escaping would map a
different name. The code is unchanged, and the behaviour is now pinned:
- test_bundled_postgres.py asks the server's own reading of the map for
the three shapes;
- the hermetic authentication-files case asserts that DOMAIN\alice is
written verbatim.
The mutant that escapes backslashes is killed by all four.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#77's head now carries #7200ff3e8, #69085ba0b and main 2fc714d, plus
T084's fix round 2 (a gate verb on the default gate is refused). None of it
touches serve.py, serve_workbench.py or serve_project.py.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…a49040#69 moved from 085ba0b to 52a2b8d. It brings fix round 16 (a local
install's served role and database are the bundle's own), and stack A's merge
of main 9a49040, where T081 (#74) landed. Merged, never rebased.
There were no conflicts. Suite: tests/ 2605 passed, 11 skipped;
tests_runtime/ 624 passed, 166 skipped.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… stack A's main
T073 merged #69's 52a2b8d: fix round 16, and stack A's merge of main
9a49040, where T081 (#74) landed. T084 therefore takes T081's change to
serve_workbench.py, the no-model refusal hoisted above the chat turn's scope
step, and keeps both sides: T081's hoist, and T084's scope seam two
unchanged lines below it.
T081'S TEST ON THIS SEAM. T082's writer prepared t084-scope-stand-in.patch.
It no longer applied, because the landed file had moved, so it is applied by
hand and checked. tests/test_chat_model_configuration.py's `scope_stand_in`
stood an `openxdox.doxbench_scope` module into sys.modules; it now registers
a scope authority at `opendox.column_seams.scope` and restores the seam's
state exactly afterwards. Before: 4 failed, 45 passed. After: 49 passed.
tests/: 2688 passed, 11 skipped.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…view)
The bundle's DSNs left search_path implicit, so PostgreSQL used its default,
"$user", public. The owner (opendox) and the served role (opendox_runtime)
are different users. So in a reused cluster holding a schema named after
either role, the migration's ledger and the served workload's reads landed
in two different schemas. That is the split-schema fault
_refuse_two_dsns_that_select_different_schemas refuses for an operator's
DSNs.
DatabaseBundle.dsn now appends options=-c search_path=public
(BUNDLE_SEARCH_PATH_OPTION, over the new BUNDLE_SCHEMA). public is the
schema _bootstrap grants in, and schema_selected_by reads it back from both
DSNs.
New real-server case: a running bundle gains schemas `opendox` (the
owner's) and `opendox_runtime` (authorization opendox_runtime). After a
restart:
- both roles' current_schema() is public;
- both read the same ledger;
- the restart applies nothing;
- status is reachable with nothing pending.
Without the pin, the restart migrates into `opendox` and fails with
RuntimeAccessMissingError. The two-DSN layout case also asserts that both
select public. Both mutants are killed: the option dropped, and the path
left at "$user", public.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot review)
_identity read every OSError from /proc/<pid> as "gone", and _serves turned
that into "not this server". But a procfs mount option or a security module
can withhold a LIVE process of this very user, a PostgreSQL server
included. So _remove_a_proven_stale_lock could unlink the postmaster.pid of
a live server, and a second server was launched beside it.
- _identity now returns None only when the entry has vanished
(FileNotFoundError or ProcessLookupError). Any other OSError raises
_Withheld, a LookupError, so _serves answers None (unknown), not False.
- _remove_a_proven_stale_lock fails closed on a withheld live pid. The lock
is kept, and the start is refused by name: it names the pid and says
"will not describe", and tells the user to stop that process or remove
the lock once they know it is not a server on this data directory.
- With no /proc at all the lock is still left for PostgreSQL to judge, as
before.
- A gone pid is still "not ours", and a lock /proc proves stale is still
removed.
New case (Linux): an existing cluster whose lock names this process's own
live pid, with that pid's /proc links withheld. _serves is None, the start
is refused by name, the lock is kept, and the stand-in server never runs.
Afterwards a gone pid reads False, and the now-describable stale lock is
removed. The case fails against 4a9dbe9's bundle.py. Both mutants are
killed: withheld read as gone, and withheld left to PostgreSQL.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#77's head now carries #72620bee2, #6952a2b8d and main 9a49040 (#74, T081,
landed), plus T084's M1, L1 and G7 rounds and `python -m opendox.serve --help`
naming openDox only. Its serve.py hunk (SERVE_PROG, SERVE_DESCRIPTION in
`main`) does not meet T103's.
Arc: neutral-product-standalone-operability
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Prevent SIGTERM interruption from orphaning PostgreSQL during shutdown
src/opendox/cli.py:703
On non-Linux POSIX platforms, restoring the default SIGTERM handler before server.stop() creates an orphaning window: a repeated SIGTERM terminates the entry point immediately, but those platforms have no PDEATHSIG to stop PostgreSQL. A second Ctrl-C can similarly interrupt stop(). Keep termination signals from interrupting the bounded shutdown (for example, temporarily block/ignore SIGINT and SIGTERM), then restore the handlers after PostgreSQL has exited.
READY at c04690f — phase 3 is open (T063 landed, #1218 → a883bbf6); T070 landed (#67 → 66ff725) and T080 landed (#63 → 1130e99, touching none of #69's files). The holder checked: validate (run 37128683499) and SonarCloud green; Copilot's latest review at exactly c04690f is "Needs a closer look" with Findings: None; 0 unresolved threads; no closing keywords in the body or any commit. Fix round 18 (search_path=public in both bundle DSNs; a live pid that /proc withholds fails closed) closes the two round-17 findings the holder accepted. Dependents #72 and #73 are retargeted to main and are not READY. Brett: draft phase 3 ahead, land in plan order when green. Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lane: openxfactory-4 (openXfactory-4-openDox_extraction)
Arc: neutral-product-standalone-operability
Plan 034 (
specs/034-opendox-standalone-operation/tasks.md, read at openxFactorymain91e4685f), phase-3 slice P3-I, install mode and the bundle:f99a2097).runtime statusblock.66ff7257). It was stacked on T070, 13.4-13.6: OPENDOX_INSTALL_MODE and generate-and-open --local (plan 034) #67's branch; the holder has retargeted it tomain, and57b7ed8fmerges main66ff7257, so this diff is T072 alone.Ruled:
5817152735;5850003126;5901112350.Claimed on openxFactory#656 in
5901575394. Authored ahead as a DRAFT, which did not go READY before T063 landed and the holder said so. T063 has landed (openxFactory#1218 →a883bbf6, 2026-10-02 23:37:43Z), which closes phase 2, so phase 3 is open. #60 (7ff434d9) and #67 (66ff7257) ahead of it have landed.What it does, by R1Q16's four parts
opendox generate-and-open --local(orOPENDOX_INSTALL_MODE=local) startspostgresas a direct child of the process serving the document surface. It usessubprocess.Popenand neverpg_ctl, which would re-parent it.runtime statusreportsdatabase_bundle(data_dir,socket_dir,pid). It reads the pid from the server's ownpostmaster.pidand believes it only while a postgres runs there.initdbruns once per data directory.init-runtime-role.sh): CONNECT, USAGE onpublic, and DML on what the owner creates, by default privileges.migrations.MigrationRunnerruns as the owner, with the served role and database declared. The run narrows the ledger to SELECT for the served role and verifies its access, exactly as a hostedruntime migratedoes.runtime migrateunderlocalmigrates the bundle too.opendox[local]extra, which isopendox[runtime]pluspixeltable-pgserver>=0.6.0(RULED, openxFactory#6565916000030item 2). Thetestextra joins it, so F9.1's.[test]install still runs every case.PR_SET_PDEATHSIGon Linux, so a SIGKILLed entry point still takes its server with it.13.1's fixed identity
OPENDOX_STATE_DIR, at<state>/postgres/dataand<state>/postgres/run.$XDG_STATE_HOME/opendox, else~/.local/state/opendox, and must be absolute, because the server's process and aruntime statusrun from elsewhere must derive the same socket.sun_pathis refused, naming the setting.listen_addressesis empty, and the socket directory is narrowed to 0700.5916000030item 3, "Peer auth + accept (Recommended)"):initdbruns with--auth-local=peer --auth-host=reject.pg_hba.confandpg_ident.conf(atomically, mode 0600).pg_hba.confholds one rule,local all all peer map=opendox, plushost … rejectfor IPv4 and IPv6.pg_ident.confmaps the running OS user, and nobody else, toopendoxandopendox_runtime.trustis put back to peer on its next start.opendoxfor migrations,opendox_runtimefor serving) over the one socket, withportspelled so a strayPGPORTcannot redirect libpq. They pass T071's three checks for the reason those exist: one dialect, one database, and never one credential in both settings.localis refused by name. It is added to T070'sHOSTED_ONLY_SETTINGS, a holder reading on openxFactory#656 that Brett may overrule.The migrations gap (assigned to T072 by the holder)
migrations/sits at the repository root, and only the image copies it (WORKDIR /app), sopip install "opendox[local]"run outside a checkout had nothing to apply.pyproject.toml's[tool.setuptools.data-files]mapsmigrations/*.sqlinto the wheel's data directory (share/opendox/migrations). The rootmigrations/does not move.config.migrations_dirresolves an unsetOPENDOX_MIGRATIONS_DIRin this order:migrationswherever the working directory has one (a checkout, or the image's/app), which is today's default, unchanged;RECORDlists (packaged_migrations_dir);migrations/.test_a_wheel_install_migrates_its_bundled_server_outside_a_checkout:--no-build-isolation,--no-index);--prefixoutside the checkout;migrations/, asserting thatopendoxis the wheel's copy and that the migrations dir is under the prefix'sshare/opendox;applied == ["0001", "0002"]).The server package (RULED, openxFactory#656
5916000030item 2: "pixeltable-pgserver (Recommended)")pixeltable-pgserver0.6.0, the maintained fork ofpgserver, uploaded 2026-07-14. Apache-2.0, as its dist-infoLICENSEand OSI classifier say. It carries PostgreSQL 16.14 under the PostgreSQL License (initdb --versionandpostgres --versionfrompixeltable_pgserver/pginstall/bin). It also carries an 18.4 underpginstall18/, which this package does not use.importlib.util.find_spec("pixeltable_pgserver")without importing it. Its own manager is not used, because:pg_ctl, against (i);atexit, which SIGTERM never runs, against (iv);readelf -d,ldd):postgresneedslibz,libpthread,librt,libdl,libm,libc;initdbneeds those lesslibzandlibdl, plus the wheel's own vendoredlibpq. That libpq resolves throughRPATH $ORIGIN/../../../pixeltable_pgserver.libsand itself needs onlylibc,libmandlibpthread;libc, and one needs the vendored libpq.So the system libraries are the C library and libz only: no system PostgreSQL, no ICU.
manylinux_2_27andmanylinux_2_28, so they need glibc 2.27 or later. That is the tag's floor. The highest GLIBC symbol any binary or module needs is 2.25, byobjdump -T.fasteners,platformdirs,psutilandtyping-extensions, which nothing here imports. All four were already pinned.pgserver0.1.4 carried PostgreSQL 16.2 and had no wheel after cp312 (Copilot r4139811507 and r4139811528, both now resolved with this ruling cited). Also rejected:postgresql-binaries, which links the system's ICU and untars at first use, andpgembed, which is PostgreSQL 17.Outside
src/andtests/pyproject.toml: thelocalextra, thetestextra joining it,setuptools>=70.1intest(the wheel test's offline build; 70.1 is the first release that builds a wheel with nowheelpackage), and the data-files map. It is a single-writer file (T057 → T072). T057, openDox's own validator and its input set (7.1, 7.1a, 7.1b, 7.2) (plan 034) #58 (T057) adds package data there, and the merge-from-main round takes it.constraints-cpython312-linux.txt, in its own commit as its header asks. It was extended under its own pins in a clean 3.12.3 venv (psutil==7.2.2,platformdirs==4.12.2,fasteners==0.20andsetuptools==84.0.0new). At84a6c041it was re-resolved in a clean environment under the pins lesspgserver. The one line that moved ispgserver==0.1.4→pixeltable-pgserver==0.6.0.deploy/— one line, and the task requires it.deploy/compose/.env.examplegainsOPENDOX_STATE_DIR=, becausetest_every_runtime_setting_is_documented_in_env_examplerequires everySETTINGSentry there. The compose stack is hosted and never reads it.docs/: untouched.serve.py(T073 adds theinstallblock) andvalidate.yml. It already installs.[runtime,test], andtestnow carrieslocal.tests/test_doxbench_entrypoint.pystands the bundle in, with a tripwire. Its cases test the model port, which reads nothing from the store (R1Q16 (ii)).test_install_mode.pyandtest_install_mode_entrypoint.pystop passing DSNs besidelocal.test_runtime_surface.pydeclaresopendox.runtime.bundlestdlib-only at import, becauseopendox.cliimports it andopendox --helpruns with no extra installed.The falsifier
F13.1's
runtime statusblock and its TCP-listener block, verbatim in their assertions (f13-1-local.sh), against a servergenerate-and-open --localstarted in the background, with no broker and no operator database, underset -euo pipefail.Since the merge round (
19e32f0c), the start is the REAL entry point. It ispython -m opendox.cli generate-and-open --local, with the validator on, over T050'stests/fixtures/plain-documentscopied into a fresh repository, as F13.1's preamble does. The stand-in driver is deleted. One deviation remains, and it does not weaken a check:Generation is stood in.Retired at19e32f0c. Until then the start went throughtests_runtime/local_entrypoint_driver.py, because this stack's base predated T055/T056's standalone generate.runtime status. The TCP-listener block reads the server's pid fromruntime status'sdatabase_bundle, not fromcaps.json./capabilities'installblock is T073's, so F13.1'scaps.jsonblock is T073's to run.The corpus is a one-file stand-in.Retired at19e32f0c: it is T050's fixture now.BEFORE is #67's head
b50e3b1; AFTER is this branch:T070's F13.1 refusal probes and F13.1's
load_settingsblock still pass on this branch (F13.1 REFUSALS + T070 PAIR: ALL PASSED).In the suite,
tests_runtime/test_bundled_postgres.pyruns the same two blocks on the same background launch, and adds three checks: the server'sPPidis the entry point's pid (i), the socket directory is 0700, and SIGTERM ends the entry point with exit 0 and the server gone (iv). Beside that it has a SIGKILL case (the parent-death backstop), the second-server refusal,runtime migrateunderlocal, the wheel case above, and the layout and refusal cases. UnderCIit fails rather than skips if the server is missing, becausevalidate.ymlpinsEXPECT_SKIPPED=11exactly.A mutant of each new refusal and guarantee, killed
Each mutant was applied alone, and
test_bundled_postgres.pyplustest_install_mode.pywere run with-x, with a 240 s bound so a hang could not pass for a kill:test_migrate_under_the_local_mode_uses_the_bundle_and_refuses_a_dsntest_a_state_dir_too_long_for_a_unix_socket_is_refused_naming_ittest_a_relative_state_dir_is_refused_naming_itlisten_addresses=127.0.0.1)test_the_entry_point_owns_a_migrated_server_with_no_tcp_listenersetsid -f)stop()a no-optest_a_second_entry_point_on_the_same_state_dir_is_refusedtest_the_server_stops_even_when_the_entry_point_is_killed_outrighttest_the_entry_point_owns_a_migrated_server_with_no_tcp_listenertest_the_two_dsns_are_two_users_over_the_one_sockettest_a_wheel_install_migrates_its_bundled_server_outside_a_checkouttest_the_entry_point_owns_a_migrated_server_with_no_tcp_listenertest_a_second_entry_point_on_the_same_state_dir_is_refusedA defect this PR's own test found in itself. The first cut of the wheel case ran
pip install --prefixwithout--ignore-installed. pip then read the suite's own editableopendoxas the installed copy of the same project and uninstalled it, emptying the environment the suite runs in (measured:pip listlostopendoxand both console scripts). The flag is now there, with a comment, and the case asserts afterwards that the suite's ownopendoxstill resolves.The repo's own suite
Full
python -m pytest -q,LANG=C.UTF-8,CI=true, against apostgres:16likevalidate.yml's:f097fd8b50e3b1525f61c, its fix round 2c3a70a232db5d8(merges525f61c)ac61596(merges02dadc5)28bdccd(fix round 4, and merges859b37b6)96b2699f(fix round 5)a0fb7c8d(merges026f00ea; fix round 6)0f77d5c1(fix round 7)379fbb14(fix round 8)84a6c041(the carrier and peer authentication, as ruled)f8e6e9e9(fix round 10)fe232fe(merges #67'sc8fac05e, carrying main047bb4fa), before its edits19e32f0c(the merge round's edits)6eb0bbdb(merges #67'scdf7382b; the env probe expects the child's own state dir)fedfa75d(fix round 11,0488f5bd, then merges #67'sd1de1fd9with no file change)f66e5f82(fix round 12,3426c753, then merges #67's105f2f12)a9854078(in-process local cases get their own state dir), with local and broker settings, a runnerOPENDOX_STATE_DIRand a 90-characterXDG_STATE_HOMEexported21bde1af(the adversarial review's M1, L1, L2, L3 and replication note)28e195b9(fix round 13: the carrier is found as the installed distribution's own files)57b7ed8f(fix round 14,bf9bcb08, then merges main66ff7257)058d96ef(fix round 15)085ba0b0(merges main2fc714d2, #62 T079)3ebccb3c(fix round 16)52a2b8dc(merges main9a490405, #74 T081)4a9dbe95(fix round 17)c04690f8(fix round 18,8986158eandc04690f8)EXPECT_SKIPPED=11holds exactly, and the floors allow the rise unchanged. The new module adds about 32 s to the run (ten cases, each server start about 1.5 s).Merging #67's fix rounds.
95fe16fmerges32683e8, and32db5d8merges525f61c:runtime migrateandresetrefuse what a local install cannot be.525f61cand this PR both rewriteload_migration_settings. The conflict resolves to this PR's structure: underlocal, the refusal is asked first, and only then is the bundle's migration DSN read. T070's reason is carried into the comment.locala DSN is itself refused here, so a case that set one would have tested the DSN refusal instead of the broker or bind refusal it names.ac61596merges02dadc5, cleanly. With the runtime extra absent,status's early return now reports a local install's broker as not configured. The merged case uses this PR's local shape and also asserts thatdatabase_bundleis reported on that return: present for local,nullfor hosted.Fix rounds 4 and 5: Copilot's twelve threads (
5e52872,96b2699f)Copilot reviewed
95fe16f,32db5d8andac61596aand opened twelve threads. Ten are fixed, answered and resolved. The new cases are intests_runtime/test_local_lifecycle.py(new, hermetic), plus two intest_bundled_postgres.py.OPENDOX_MIGRATIONS_DIRis used as given.__file__came from first, then theRECORDof the distribution that holds the running module. Where there is none, it is refused./procproves it is an executable namedpostgresrunning in this data directory. A proven-stale lock is removed before the launch./proc, nothing is believed, and PostgreSQL's own interlocks stand. The price is astatuswith no pid on macOS and the BSDs, recorded in the thread.data/is refused and left alone.~user, or no home, refuses namingOPENDOX_STATE_DIR.Evidence:
ac61596's source, 17 of round 4's first 18 cases fail; the one that passes is the unchanged hosted default. Round 5's cases fail against28bdccd.runs/mutants-t072-r4.txt,-r5.txtin the writer's workdir).Round 6 (
a0fb7c8d): Copilot's review at28bdccd9opened two more threads._servespoint was already fixed at96b2699f; it is answered and resolved.installation_migrations_dir, and the packaging case checks everyopendox.runtime.config.<name>pyproject names.4aed6278merges T070's026f00ea, a docstring change.Round 7 (
0f77d5c1): Copilot's review ata0fb7c8dopened three threads, all fixed and resolved. SonarCloud raised one reliability finding.PG*variable is lifted out ofos.environfor the duration and put back afterwards, aroundgenerate-and-open --local's lifecycle and the runtime verbs underlocal.PGHOSTADDR,PGSERVICEandPGOPTIONScan no longer redirect the bundle's connections. The real entry point andstatusare proven with all three set.server_binariesno longer indexes a list.a0fb7c8d, and 11 mutants are killed.Round 8 (
379fbb14): Copilot's review at0f77d5c1opened two threads, both fixed and resolved...anywhere.0f77d5c1, and 5 mutants are killed.Round 9 (
84a6c041) applies Brett's two rulings, openxFactory#6565916000030items 2 and 3: the carrier and peer authentication, as described above.pg_hba_file_ruleshas exactly the one peer rule (withmap=opendox) and the two rejects;pg_ident_file_mappingshas exactly the two mappings, for this OS user;system_userispeer:<os user>for both roles.peer authentication failed). A non-root suite cannot connect as a second OS user, so for that case the map, read back from the server, stands: it names no other user.379fbb14. 9 mutants are killed:The auth mutants are also killed by the real-server cases alone.
Round 10 (
f8e6e9e9): Copilot's review at84a6c041raised three points, all fixed.postgres/datajoins the tree check, a broken link included (r4147680113, resolved).mkdir(parents=True)under umask 0002 made them group-writable.84a6c041, and 4 mutants are killed.SonarCloud S2115, ACCEPTED, as ruled.
AaDvyyOCiqwq-gAw53M3, python:S2115, "Add password protection to this database", onsrc/opendox/runtime/config.pyDatabaseBundle.dsn.accept(SonarCloud now reports itRESOLVED). Set with the SonarQube tool on openxFactory#6565916000030item 3's authority.SO_PEERCRED), andpg_ident.confmaps only this install's OS user to the two roles. The socket directory is 0700, the server has no TCP listener (listen_addressesis empty), and every host connection is rejected. The same rationale is in the DSN's docstring.OKon every condition.Merge-from-main round (
fe232fe,19e32f0c; 2026-10-02)Phase 2 has landed. This branch now carries #67's
c8fac05e, which carries #60'sadeb6fedand main047bb4fa(T054 to T058, T055's follow-up #70 and T056's standalone test). Git auto-mergespyproject.toml(main's validator package data beside this PR's local extra and data files),src/opendox/cli.pyandtests/test_doxbench_entrypoint.pywithout a conflict. Four edits followed, all in19e32f0c:tests_runtime/local_entrypoint_driver.pygo. The driver is deleted. Its stand-ins patched names T055 has since replaced, so on the merged tree they stood in for nothing, and all three background cases failed: the real corpus-root check refused the stand-in corpus.test_bundled_postgres.pynow runs the real entry point over T050's fixture, with the validator on._refuse_empty_source_options, so the local path asks it before it builds the bundled server.tests/test_projection_seams.py's empty-option case carries a tripwire bundle, so a regression neither starts a server nor passes.--local, and here--localstarts the bundled server, whoseOPENDOX_STATE_DIRdefaults to the user's~/.local/state/opendox.tests/standalone_child.pynow gives every child a fresh, short, private state directory under/tmpand removes it when the child stops. Measured before: three children of T056 and T058 initialized a cluster in the (sandboxed) default state home.3195 selected, 3184 passed, 11 skipped, 0 failed. Nothing is left under~/.local/state/opendoxor/tmp/odx-child-*.Merge of #67's fix rounds (
94254b18,6eb0bbdb; 2026-10-02)94254b18merges #67'scdf7382b, which carries #60'sc39d960e(a PostgreSQL scheme libpq would not read as a URI is refused).cdf7382bitself means a--localcaller inherits none of the runner's runtime settings. There were two docstring and setup conflicts, and both were resolved by keeping both sides:tests/standalone_child.py: the code merged cleanly in the needed order. The child's environment first drops everySETTING_NAMESentry, and only then isOPENDOX_STATE_DIRset to the child's own directory.tests/test_projection_seams.py: the empty-option case scrubs the settings and keeps this PR's bundled-server tripwire.6eb0bbdbchanges #67's harness probe. On #67 it asserted that a child sees no runtime setting. Here every child is given exactly one, its private state directory. The probe now also exports a runner state directory, and asserts three things:OPENDOX_STATE_DIRamong the runtime settings;Child.state_dir, not the runner's;Evidence:
tests/test_standalone_generate_path.py,tests/test_post_render_validator.py) pass whole with a hosted install's settings and a runnerOPENDOX_STATE_DIRexported.3204 selected, 3193 passed, 11 skipped, 0 failed. Nothing is left under~/.local/state/opendoxor/tmp/odx-child-*.Fix round 11: nothing is made through a path the tree check would refuse (
0488f5bd, thenfedfa75d)Copilot's review at
19e32f0copened one thread, which is real (reproduced) and is now answered and resolved._prepare_directoriesmade the missingpostgres/runbefore the tree check judged the path, so a component it refuses had already been written through: another user's link, or a 0777 directory. In a sticky parent such as/tmp, another user could also plant the state directory's name between the check and themkdir. The fix:_refuse_an_unsafe_tree(existing_only=True)runs the link-ownership loop first, so even a broken foreign link is named. The whole tree is judged again afterwards, before the socket directory'schmod._make_private_directoriesmakes each one relative to its parent's descriptor and opens it withO_NOFOLLOW.fstatmust show it is this user's alone before anything is made beneath it. A planted link, non-directory or foreign directory is the named refusal: never followed, never re-moded.mkdir/chmodwindow. Each component is born 0700 under a umask of 077, and the umask is put back afterwards.Evidence:
tests_runtime/test_local_lifecycle.py. All fail against19e32f0c'sbundle.pyand pass here.3210 selected, 3199 passed, 11 skipped, 0 failed.fedfa75dmerges #67'sd1de1fd9(its healthy-local status case reads either DSN form). On this branch that case uses the bundled server, so the conflict resolves to this side and changes no file.Fix round 12: the auth files are exactly 0600, and the cluster runs on its own files (
3426c753, thenf66e5f82)Copilot's reviews at
6eb0bbdbandfedfa75dopened two threads, both real and now answered and resolved.write_authentication's 0600 was only a creation request. The umask filtered it, and a stale temporary from an interrupted start kept its own mode or was written through as a link. Now the stale temporary is unlinked, the new one is openedO_CREAT | O_EXCL | O_NOFOLLOW, and its descriptor isfchmod-ed to exactly 0600 before anything is written. A link raced in after the unlink is a refusal, never followed.postgresql.confcould redirectdata_directory,hba_fileandident_file, for example to an outsidetrustfile. The launch now pins all three on the command line, which outranks every configuration file.Evidence:
tests_runtime/test_local_lifecycle.py(umask, stale 0644, stale link, raced link) and one real-cluster case intests_runtime/test_bundled_postgres.py. All but the raced-link case fail againstfedfa75d.3215 selected, 3204 passed, 11 skipped, 0 failed.f66e5f82merges #67's105f2f12cleanly. It adds an autouse fixture intests_runtime/conftest.pythat clears every runtime setting before each case, so a case wantingOPENDOX_STATE_DIRsets its own, as every one here already does.The adversarial review of
f66e5f82(a9854078to21bde1af; 2026-10-02)An adversarial review of
f66e5f82found nothing high. It found one medium, three lows and a note. Each is fixed in its own commit, each with a new case that fails without it and mutants that are killed. One more hermeticity fix came first.a9854078: the in-process local cases give themselves a short state directory. The doxBench entrypoint fixture and the empty-option case intest_projection_seams.pyscrubbed the settings, and so fell back to the runner's default state directory. When that is too long for a Unix socket, configuration refuses it before the case is reached. Measured with a 90-characterXDG_STATE_HOME: 4 errors and 1 failure. Each now sets its own shortOPENDOX_STATE_DIRand removes it afterwards. Nothing is made in it, because the database is stood in. Both mutants are killed.e214477d): a comma in the state directory is refused. PostgreSQL splits-kon commas, and libpq splits a decodedhoston them. The review reproduced sockets in two unchecked directories, one of them 0777, while the checked 0700 directory stayed empty.config.database_bundle(every bundle's one derivation) now refuses a,fromOPENDOX_STATE_DIR,XDG_STATE_HOMEorHOME, without repeating the value.c4f5df3c): the carrier is pinned topixeltable-pgserver>=0.6.0,<0.7, and another major is refused by name. Before an existing cluster is used, the server's ownpostgres --versionis asked against itsPG_VERSION. Another major, or a server that does not say, is a named refusal, before anything is written. 4 mutants are killed.b2d80e94): the directory creation starts from is judged by its descriptor._make_private_directoriesnowfstat-judges the base it opens with_unsafe_becausebefore the firstmkdir. That is the install's own rule for the state directory and below, and the ancestors' rule above it. This makes round 11's rule hold inside the function itself. 2 mutants are killed.9e2f3030): the local verbs judge their socket before connecting to it.runtime status,migrateandresetconnected to whatever answered at the bundle's socket path. Reproduced here:statuson a 0777 tree whoserunlinked to another bundle's socket reported that bundle's applied migrations and exited 0. The fix:bundle.refusal_before_connectingasks the start's tree check (nowbundle.refuse_an_unsafe_tree) of what exists. It then asks for a live server of THIS data directory: its ownpostmaster.pidmust name a livepostgreswhose working directory is this data directory, listening at this socket directory.databasereads"not probed: <reason>"for a local install that fails this check, including one with no server running. It used to read"unreachable: …", found by connecting.migrateandresetrefuse aslocal-bundle-unverified. Hosted is unchanged.git diff -wshows only the branch.21bde1af): no replication connection, logical or physical. Fixed, not only reworded.authentication_files' docstring said a replication connection is refused. A physical one was refused, but a logical one (replication=database) was accepted aspeer:<user>, andIDENTIFY_SYSTEManswered (measured). Nopg_hba.confrule can tell it from an ordinary connection. So the launch setsmax_wal_senders=0, and the docstring now says both kinds are refused by the server. The mutant is killed.Full suite:
3236 selected, 3225 passed, 11 skipped, 0 failed. Nothing is left under~/.local/state/opendoxor/tmp/odx-*.Fix round 13: the server is found as the installed distribution's own files (
28e195b9)Copilot's review at
f66e5f82opened one thread, which is real and is now answered and resolved.server_binariesusedimportlib.util.find_spec, which followssys.path. Underpython -m opendox.clithat starts with the working directory, and a corpus checkout is where it runs. So a checkout holding an executablepixeltable_pgserver/pginstall/bin/postgreswas run as the database server.The carrier is now looked up by distribution name (
importlib.metadata), onsys.pathwithout the working directory. Both binaries must be files its RECORD lists, inside it and executable.Evidence:
.dist-info, and the server is not taken from it.find_spec.3240 selected, 3229 passed, 11 skipped, 0 failed.Fix round 14: a named platform gate, resolution failures as reasons, no pid behind a refused tree (
bf9bcb08)Copilot's review at
21bde1afopened three threads, all real and now answered and resolved.AttributeError.bundle.unsupported_platform()names the missing primitives (os.getuid,O_DIRECTORY,O_NOFOLLOW,os.fchmod,mkdirwithdir_fd,socket.AF_UNIX). A start and the local verbs' socket check ask it first.refusal_before_connecting()names a symlink loop (RuntimeError) and an embedded NUL (ValueError) as reasons, besideOSError.bundle.report()reports a pid only behind a verified tree. Apostgres/datalinked to another live bundle had reported that server's pid.Evidence:
linked-datashape with null-pid assertions in the real verbs case.Merge of main after #67 landed (
57b7ed8f; 2026-10-03)#67 (T070) landed as a squash,
66ff7257, after #61 (T078,8a98e317). Main thus holds T070's content as one commit this branch's history never saw, and a plain merge against the old base047bb4faconflicted in ten files where both sides carry the same T070 text.So the merge is computed against the T070 state this branch already held, #67's
105f2f12:git merge-tree --write-tree --merge-base=105f2f12 HEAD origin/main. It is recorded with both parents. The base is sound becausegit diff 25262459 66ff7257(#67's last branch head against its squash) names only #61's three files. Against it the merge is clean:105f2f12: T088, the lens's two seed actions are offered only where a binding answers them (R1Q19 (a)) (plan 034) #65 T088, T085, the standalone doxBench defaults for T027's seams, and T058's two workbench rules move into opendox.validator (plan 034) #71 T085, T078, 16.1: the OpenAI-compatible dialect joins DIALECTS (plan 034) #61 T078, and T070, 13.4-13.6: OPENDOX_INSTALL_MODE and generate-and-open --local (plan 034) #67's--localin T085, the standalone doxBench defaults for T027's seams, and T058's two workbench rules move into opendox.validator (plan 034) #71's test.src/opendox/cli.pyis the one merged file: main's twodoxbench_defaultsregistrations sit beside this branch's local path.generate-and-openchild main brought in lacks--local. T085, the standalone doxBench defaults for T027's seams, and T058's two workbench rules move into opendox.validator (plan 034) #71's case gained it in T070, 13.4-13.6: OPENDOX_INSTALL_MODE and generate-and-open --local (plan 034) #67's merge. T088, the lens's two seed actions are offered only where a binding answers them (R1Q19 (a)) (plan 034) #65's lens test runsgenerateandserve.build_server. T078, 16.1: the OpenAI-compatible dialect joins DIALECTS (plan 034) #61 runs no entry point. Under T072, T085, the standalone doxBench defaults for T027's seams, and T058's two workbench rules move into opendox.validator (plan 034) #71's--localchild starts a bundled server intests/standalone_child.py's private state directory.3327 selected, 3316 passed, 11 skipped, 0 failed.Fix round 15: on Linux the parent-death signal is armed, or the server is not started (
058d96ef)Copilot's review at
57b7ed8fopened one thread, which is real and is now answered and resolved.ctypesreports a failedprctl()by returning-1, never by raising (a seccomp denial, say), and the child ignored it. A killed entry point could then orphan the server, against R1Q16 (iv).The fix:
prctlis a refusal. The child now raises on a nonzero return.subprocessre-raises that asSubprocessError, which_launchnames as the refusal "could not be given its parent-death signal", with no server process left.prctlis the same refusal, where it used to fall back silently. Other platforms are unchanged.Evidence:
prctlthat returns-1. The start refuses by name, and the stand-in server never runs.3328 selected, 3317 passed, 11 skipped, 0 failed.Merge of main after #62 landed (
085ba0b0; 2026-10-03)085ba0b0merges main2fc714d2(#62, T079). #62 changes onlydoxbench_binding.py,doxbench_intake.py,doxbench_provider.pyandtest_model_provider_broker.py, none of which this branch touches, so the merge is clean. It runs nogenerate-and-openchild, so it needs no--local. Full suite:3346 selected, 3335 passed, 11 skipped, 0 failed.Fix round 16: a local install's served role and database are the bundle's own (
3ebccb3c)Copilot's review at
085ba0b0opened one thread, which is real and is now answered and resolved. Underlocal,OPENDOX_RUNTIME_PG_ROLEcould replace the bundle's served role in the settings. The migration run narrows the ledger privileges of exactly that configured role, while the bundle bootstrapsopendox_runtimewith the default DML and its served DSN connects asopendox_runtime. SoOPENDOX_RUNTIME_PG_ROLE=pg_read_all_dataleftopendox_runtimeable to rewriteopendox_schema_migrations.refuse_what_a_local_install_cannot_be(asked by both loaders and bygenerate-and-open --local) now accepts two settings only unset or naming the bundle's own:OPENDOX_RUNTIME_PG_ROLEmust beopendox_runtime;OPENDOX_SERVED_DATABASEmust beopendox, since it declares the same identity.Hosted is unchanged.
Evidence:
3350 selected, 3339 passed, 11 skipped, 0 failed.Merge of main after #74 landed (
52a2b8dc; 2026-10-03)52a2b8dcmerges main9a490405(#74, T081), with no overlap. #74's standalonegenerate-and-openfixture already passes--local, since it landed after #67. Under T072 that child now starts a bundled server in the harness's private state directory.tests/test_chat_model_configuration.py: 49 passed.3399 selected, 3388 passed, 11 skipped, 0 failed.Fix round 17: the
/procfalsifier is gated, and a backslash in the map is pinned literal (4a9dbe95)Copilot's review at
52a2b8dcopened two threads, both now answered and resolved./procfalsifier is gated.test_the_entry_point_owns_a_migrated_server_with_no_tcp_listenerreads Linux's/proc, so it now skips where/procis absent, as the other/procand PDEATHSIG cases do. On Linux, and in CI, nothing changes.pg_ident.conf. Measured against the bundled PostgreSQL 16.14,pg_ident_file_mappingsreads"DOMAIN\alice","alice\"and"a\\b"back as exactly those names, without error. The tokenizer treats a backslash specially only at the end of a line, never inside quotes, so escaping would map a different name.Full suite:
3402 selected, 3391 passed, 11 skipped, 0 failed.main has since taken #63 (T080,
1130e996), whose five files (the doxbench model-provider family) do not overlap this branch. The PR stays MERGEABLE, and CI's merge ref includes it.Fix round 18: one schema for both DSNs, and an uninspectable live pid fails closed (
8986158e,c04690f8)Copilot's review at
4a9dbe95opened two threads. The holder accepted both, and both are now answered and resolved.8986158e: both bundle DSNs namesearch_path=public. Left implicit, PostgreSQL's"$user", publiclet a reused cluster with a schema namedopendoxoropendox_runtimesplit the owner's ledger from the served role's reads.current_schema()ispublic, they read one ledger, nothing is re-applied, andstatusis reachable with nothing pending.RuntimeAccessMissingError.c04690f8: a live pid/procwill not describe is unknown, not "not ours"._identityraises_Withheldfor anything but a vanished entry, and_remove_a_proven_stale_lockkeeps the lock and refuses the start by name rather than unlinking a possibly live server's lock. With no/procat all, PostgreSQL still judges./proclinks are withheld. It fails against4a9dbe95.Full suite:
3404 selected, 3393 passed, 11 skipped, 0 failed, run withLANG=C.UTF-8and noGIT_*/XF_*.Downstream, for the holder
serve.pycannot bind::1. This is pre-existing, measured at T071, 13.2 and 13.3: load_settings refuses a non-PostgreSQL DSN and a collapsed DSN pair (plan 034) #60's headf097fd8:gaierror [Errno -9] Address family for hostname not supported.serve.LOOPBACK_HOSTSlists::1, butThreadingHTTPServeris IPv4. Recorded on T070, 13.4-13.6: OPENDOX_INSTALL_MODE and generate-and-open --local (plan 034) #67 (T070) for aserve.pyfollow-up in that file's single-writer order. This PR does not touchserve.py.tests/fixtures/plain-documents.--localchild built withtests/standalone_child.pygets its own state directory automatically. Any other--localcaller must setOPENDOX_STATE_DIRitself.database_bundlefrom the server object the entry point keeps (args.database_bundle) for/capabilities'installblock.caps.jsonblock included.gh pr diff -R opensoft/openDox-code):pyproject.toml(T057, openDox's own validator and its input set (7.1, 7.1a, 7.1b, 7.2) (plan 034) #58) andcli.py(T055, serve and generate standalone (5.5, 4.3 part) (plan 034) #59, and the T058 writer after it). They have landed, and this stack took its merge-from-main round after them.serve.pyis not touched here.🤖 Generated with Claude Code
Summary by Sourcery
Add a self-contained local installation mode that owns, migrates, reports, and cleans up its bundled PostgreSQL server.
New Features:
Bug Fixes:
Enhancements:
Build:
Deployment:
Tests: